diff --git a/.gitignore b/.gitignore index 25c1195923..b648ad00b9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ var/ local_settings.py *.sqlite3 *.backup +*.old # Sphinx files docs/_build diff --git a/InvenTree/InvenTree/static/script/inventree/company.js b/InvenTree/InvenTree/static/script/inventree/company.js new file mode 100644 index 0000000000..86974630ef --- /dev/null +++ b/InvenTree/InvenTree/static/script/inventree/company.js @@ -0,0 +1,188 @@ + +function loadCompanyTable(table, url, options={}) { + /* + * Load company listing data into specified table. + * + * Args: + * - table: Table element on the page + * - url: Base URL for the API query + * - options: table options. + */ + + // Query parameters + var params = options.params || {}; + + var filters = loadTableFilters("company"); + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList("company", $(table)); + + $(table).inventreeTable({ + url: url, + method: 'get', + queryParams: filters, + groupBy: false, + formatNoMatches: function() { return "No company information found"; }, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + }, + { + field: 'name', + title: 'Company', + sortable: true, + formatter: function(value, row, index, field) { + var html = imageHoverIcon(row.image) + renderLink(value, row.url); + + if (row.is_customer) { + html += ``; + } + + if (row.is_manufacturer) { + html += ``; + } + + if (row.is_supplier) { + html += ``; + } + + return html; + } + }, + { + field: 'description', + title: 'Description', + sortable: true, + }, + { + field: 'website', + title: 'Website', + formatter: function(value, row, index, field) { + if (value) { + return renderLink(value, value); + } + return ''; + } + }, + ], + }); +} + + +function loadSupplierPartTable(table, url, options) { + /* + * Load supplier part table + * + */ + + // Query parameters + var params = options.params || {}; + + // Load 'user' filters + var filters = loadTableFilters("supplier-part"); + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList("supplier-part", $(table)); + + $(table).inventreeTable({ + url: url, + method: 'get', + queryParams: filters, + groupBy: false, + formatNoMatches: function() { return "No supplier parts found"; }, + columns: [ + { + checkbox: true, + }, + { + sortable: true, + field: 'part_detail.full_name', + title: '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 += `INACTIVE`; + } + + return html; + } + }, + { + sortable: true, + field: 'supplier', + title: "Supplier", + formatter: function(value, row, index, field) { + if (value) { + var name = row.supplier_detail.name; + var url = `/company/${value}/`; + var html = imageHoverIcon(row.supplier_detail.image) + renderLink(name, url); + + return html; + } else { + return "-"; + } + }, + }, + { + sortable: true, + field: 'SKU', + title: "Supplier Part", + formatter: function(value, row, index, field) { + return renderLink(value, row.url); + } + }, + { + sortable: true, + field: 'manufacturer', + title: 'Manufacturer', + formatter: function(value, row, index, field) { + if (value) { + 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: 'MPN', + }, + { + field: 'link', + title: 'Link', + formatter: function(value, row, index, field) { + if (value) { + return renderLink(value, value); + } else { + return ''; + } + } + }, + ], + }); +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/script/inventree/filters.js b/InvenTree/InvenTree/static/script/inventree/filters.js index f59645bf8a..de04bcd86d 100644 --- a/InvenTree/InvenTree/static/script/inventree/filters.js +++ b/InvenTree/InvenTree/static/script/inventree/filters.js @@ -17,6 +17,7 @@ function defaultFilters() { stock: "cascade=1", build: "", parts: "cascade=1", + company: "", }; } @@ -72,8 +73,6 @@ function saveTableFilters(tableKey, filters) { var filterstring = strings.join('&'); - console.log(`Saving filters for table '${tableKey}' - ${filterstring}`); - inventreeSave(lookup, filterstring); } @@ -255,12 +254,8 @@ function setupFilterList(tableKey, table, target) { var clear = `filter-clear-${tableKey}`; var make = `filter-make-${tableKey}`; - console.log(`Generating filter list: ${tableKey}`); - var filters = loadTableFilters(tableKey); - console.log("Filters: " + filters.count); - var element = $(target); // One blank slate, please diff --git a/InvenTree/InvenTree/static/script/inventree/order.js b/InvenTree/InvenTree/static/script/inventree/order.js index f0d2663b32..12b2fd4333 100644 --- a/InvenTree/InvenTree/static/script/inventree/order.js +++ b/InvenTree/InvenTree/static/script/inventree/order.js @@ -114,7 +114,7 @@ function loadPurchaseOrderTable(table, options) { setupFilterList("order", table); - table.inventreeTable({ + $(table).inventreeTable({ url: options.url, queryParams: filters, groupBy: false, diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index d927d0fbe4..bf0ddbef5e 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -228,8 +228,16 @@ function loadStockTable(table, options) { name += " | "; name += row.part__revision; } + + var url = ''; + + if (row.supplier_part) { + url = `/supplier-part/${row.supplier_part}/`; + } else { + url = `/part/${row.part}/`; + } - return imageHoverIcon(row.part__thumbnail) + renderLink(name, '/part/' + row.part + '/stock/'); + return imageHoverIcon(row.part__thumbnail) + renderLink(name, url); } }, { diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 44c71ea9ee..a07a8d6f99 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -15,8 +15,6 @@ class StatusCode: Render the value as a label. """ - print("Rendering:", key, cls.options) - # If the key cannot be found, pass it back if key not in cls.options.keys(): return key diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 9cdfecbd8a..e1258385a5 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -115,9 +115,12 @@ class AjaxMixin(object): # (this can be overridden by a child class) ajax_template_name = 'modal_form.html' - ajax_form_action = '' ajax_form_title = '' + def get_form_title(self): + """ Default implementation - return the ajax_form_title variable """ + return self.ajax_form_title + def get_param(self, name, method='GET'): """ Get a request query parameter value from URL e.g. ?part=3 @@ -169,7 +172,7 @@ class AjaxMixin(object): else: context['form'] = None - data['title'] = self.ajax_form_title + data['title'] = self.get_form_title() data['html_form'] = render_to_string( self.ajax_template_name, diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 8b777dd947..19ae54af32 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -10,6 +10,7 @@ from rest_framework import filters from rest_framework import generics, permissions from django.conf.urls import url, include +from django.db.models import Q from InvenTree.helpers import str2bool @@ -43,9 +44,10 @@ class CompanyList(generics.ListCreateAPIView): ] filter_fields = [ - 'name', 'is_customer', + 'is_manufacturer', 'is_supplier', + 'name', ] search_fields = [ @@ -80,22 +82,40 @@ class SupplierPartList(generics.ListCreateAPIView): queryset = SupplierPart.objects.all().prefetch_related( 'part', - 'part__category', - 'part__stock_items', - 'part__bom_items', - 'part__builds', 'supplier', - 'pricebreaks') + 'manufacturer' + ) + + def get_queryset(self): + + queryset = super().get_queryset() + + # Filter by EITHER manufacturer or supplier + company = self.request.query_params.get('company', None) + + if company is not None: + queryset = queryset.filter(Q(manufacturer=company) | Q(supplier=company)) + + return queryset def get_serializer(self, *args, **kwargs): # Do we wish to include extra detail? try: - part_detail = str2bool(self.request.GET.get('part_detail', None)) + kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None)) except AttributeError: - part_detail = None + pass + + try: + kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', None)) + except AttributeError: + pass - kwargs['part_detail'] = part_detail + try: + kwargs['manufacturer_detail'] = str2bool(self.request.query_params.get('manufacturer_detail', None)) + except AttributeError: + pass + kwargs['context'] = self.get_serializer_context() return self.serializer_class(*args, **kwargs) @@ -114,13 +134,14 @@ class SupplierPartList(generics.ListCreateAPIView): filter_fields = [ 'part', - 'supplier' + 'supplier', + 'manufacturer', ] search_fields = [ 'SKU', 'supplier__name', - 'manufacturer', + 'manufacturer__name', 'description', 'MPN', ] @@ -170,15 +191,15 @@ supplier_part_api_urls = [ url(r'^(?P\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'), # Catch anything else - url(r'^.*$', SupplierPartList.as_view(), name='api-part-supplier-list'), + url(r'^.*$', SupplierPartList.as_view(), name='api-supplier-part-list'), ] company_api_urls = [ - url(r'^part/?', include(supplier_part_api_urls)), + url(r'^part/', include(supplier_part_api_urls)), - url(r'^price-break/?', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'), + url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'), url(r'^(?P\d+)/?', CompanyDetail.as_view(), name='api-company-detail'), diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 85424eb3a1..003cfaf495 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -26,8 +26,9 @@ class EditCompanyForm(HelperForm): 'phone', 'email', 'contact', - 'is_customer', 'is_supplier', + 'is_manufacturer', + 'is_customer', ] @@ -58,7 +59,6 @@ class EditSupplierPartForm(HelperForm): 'base_cost', 'multiple', 'packaging', - # 'lead_time' ] diff --git a/InvenTree/company/migrations/0015_company_is_manufacturer.py b/InvenTree/company/migrations/0015_company_is_manufacturer.py new file mode 100644 index 0000000000..b1e74667c2 --- /dev/null +++ b/InvenTree/company/migrations/0015_company_is_manufacturer.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-04-12 23:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0014_auto_20200407_0116'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='is_manufacturer', + field=models.BooleanField(default=False, help_text='Does this company manufacture parts?'), + ), + ] diff --git a/InvenTree/company/migrations/0016_auto_20200412_2330.py b/InvenTree/company/migrations/0016_auto_20200412_2330.py new file mode 100644 index 0000000000..cec6f5a219 --- /dev/null +++ b/InvenTree/company/migrations/0016_auto_20200412_2330.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-04-12 23:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0015_company_is_manufacturer'), + ] + + operations = [ + migrations.AlterField( + model_name='company', + name='is_manufacturer', + field=models.BooleanField(default=False, help_text='Does this company manufacture parts?'), + ), + ] diff --git a/InvenTree/company/migrations/0017_auto_20200413_0320.py b/InvenTree/company/migrations/0017_auto_20200413_0320.py new file mode 100644 index 0000000000..0c13496713 --- /dev/null +++ b/InvenTree/company/migrations/0017_auto_20200413_0320.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-04-13 03:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0016_auto_20200412_2330'), + ] + + operations = [ + migrations.RenameField( + model_name='supplierpart', + old_name='manufacturer', + new_name='manufacturer_name', + ), + ] diff --git a/InvenTree/company/migrations/0018_supplierpart_manufacturer.py b/InvenTree/company/migrations/0018_supplierpart_manufacturer.py new file mode 100644 index 0000000000..48c4461379 --- /dev/null +++ b/InvenTree/company/migrations/0018_supplierpart_manufacturer.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.10 on 2020-04-13 03:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0017_auto_20200413_0320'), + ] + + operations = [ + migrations.AddField( + model_name='supplierpart', + name='manufacturer', + field=models.ForeignKey(blank=True, help_text='Select manufacturer', limit_choices_to={'is_manufacturer': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='manufactured_parts', to='company.Company'), + ), + ] diff --git a/InvenTree/company/migrations/0019_auto_20200413_0642.py b/InvenTree/company/migrations/0019_auto_20200413_0642.py new file mode 100644 index 0000000000..2c120b30b4 --- /dev/null +++ b/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -0,0 +1,275 @@ +# Generated by Django 2.2.10 on 2020-04-13 06:42 + +import os +from rapidfuzz import fuzz + +from django.db import migrations +from company.models import Company, SupplierPart +from django.db.utils import OperationalError, ProgrammingError + + +def clear(): + os.system('cls' if os.name == 'nt' else 'clear') + + +def reverse_association(apps, schema_editor): + """ + This is the 'reverse' operation of the manufacturer reversal. + This operation is easier: + + For each SupplierPart object, copy the name of the 'manufacturer' field + into the 'manufacturer_name' field. + """ + + # Exit if there are no SupplierPart objects + # This crucial otherwise the unit test suite fails! + if SupplierPart.objects.count() == 0: + print("No SupplierPart objects - skipping") + return + + print("Reversing migration for manufacturer association") + + try: + for part in SupplierPart.objects.all(): + if part.manufacturer is not None: + part.manufacturer_name = part.manufacturer.name + + part.save() + + except (OperationalError, ProgrammingError): + # An exception might be called if the database is empty + pass + + +def associate_manufacturers(apps, schema_editor): + """ + This migration is the "middle step" in migration of the "manufacturer" field for the SupplierPart model. + + Previously the "manufacturer" field was a simple text field with the manufacturer name. + This is quite insufficient. + The new "manufacturer" field is a link to Company object which has the "is_manufacturer" parameter set to True + + This migration requires user interaction to create new "manufacturer" Company objects, + based on the text value in the "manufacturer_name" field (which was created in the previous migration). + + It uses fuzzy pattern matching to help the user out as much as possible. + """ + + # Exit if there are no SupplierPart objects + # This crucial otherwise the unit test suite fails! + if SupplierPart.objects.count() == 0: + print("No SupplierPart objects - skipping") + return + + # Link a 'manufacturer_name' to a 'Company' + links = {} + + # Map company names to company objects + companies = {} + + for company in Company.objects.all(): + companies[company.name] = company + + # List of parts which will need saving + parts = [] + + + def link_part(part, name): + """ Attempt to link Part to an existing Company """ + + # Matches a company name directly + if name in companies.keys(): + print(" -> '{n}' maps to existing manufacturer".format(n=name)) + part.manufacturer = companies[name] + part.save() + return True + + # Have we already mapped this + if name in links.keys(): + print(" -> Mapped '{n}' -> '{c}'".format(n=name, c=links[name].name)) + part.manufacturer = links[name] + part.save() + return True + + # Mapping not possible + return False + + def create_manufacturer(part, input_name, company_name): + """ Create a new manufacturer """ + + company = Company(name=company_name, description=company_name, is_manufacturer=True) + + company.is_manufacturer = True + + # Map both names to the same company + links[input_name] = company + links[company_name] = company + + companies[company_name] = company + + # Save the company BEFORE we associate the part, otherwise the PK does not exist + company.save() + + # Save the manufacturer reference link + part.manufacturer = company + part.save() + + print(" -> Created new manufacturer: '{name}'".format(name=company_name)) + + + def find_matches(text, threshold=65): + """ + Attempt to match a 'name' to an existing Company. + A list of potential matches will be returned. + """ + + matches = [] + + for name in companies.keys(): + # Case-insensitive matching + ratio = fuzz.partial_ratio(name.lower(), text.lower()) + + if ratio > threshold: + matches.append({'name': name, 'match': ratio}) + + if len(matches) > 0: + return [match['name'] for match in sorted(matches, key=lambda item: item['match'], reverse=True)] + else: + return [] + + + def map_part_to_manufacturer(part, idx, total): + + name = str(part.manufacturer_name) + + # Skip empty names + if not name or len(name) == 0: + return + + # Can be linked to an existing manufacturer + if link_part(part, name): + return + + # Find a list of potential matches + matches = find_matches(name) + + clear() + + # Present a list of options + print("----------------------------------") + print("Checking part {idx} of {total}".format(idx=idx+1, total=total)) + print("Manufacturer name: '{n}'".format(n=name)) + print("----------------------------------") + print("Select an option from the list below:") + + print("0) - Create new manufacturer '{n}'".format(n=name)) + print("") + + for i, m in enumerate(matches[:10]): + print("{i}) - Use manufacturer '{opt}'".format(i=i+1, opt=m)) + + print("") + print("OR - Type a new custom manufacturer name") + + + while (1): + response = str(input("> ")).strip() + + # Attempt to parse user response as an integer + try: + n = int(response) + + # Option 0) is to create a new manufacturer with the current name + if n == 0: + + create_manufacturer(part, name, name) + return + + # Options 1) -> n) select an existing manufacturer + else: + n = n - 1 + + if n < len(matches): + # Get the company which matches the selected options + company_name = matches[n] + company = companies[company_name] + + # Ensure the company is designated as a manufacturer + company.is_manufacturer = True + company.save() + + # Link the company to the part + part.manufacturer = company + part.save() + + # Link the name to the company + links[name] = company + links[company_name] = company + + print(" -> Linked '{n}' to manufacturer '{m}'".format(n=name, m=company_name)) + + return + + except ValueError: + # User has typed in a custom name! + + if not response or len(response) == 0: + # Response cannot be empty! + print("Please select an option") + + # Double-check if the typed name corresponds to an existing item + elif response in companies.keys(): + link_part(part, companies[response]) + return + + elif response in links.keys(): + link_part(part, links[response]) + return + + # No match, create a new manufacturer + else: + create_manufacturer(part, name, response) + return + + clear() + print("") + clear() + + print("---------------------------------------") + print("The SupplierPart model needs to be migrated,") + print("as the new 'manufacturer' field maps to a 'Company' reference.") + print("The existing 'manufacturer_name' field will be used to match") + print("against possible companies.") + print("This process requires user input.") + print("") + print("Note: This process MUST be completed to migrate the database.") + print("---------------------------------------") + print("") + + input("Press to continue.") + + clear() + + part_count = SupplierPart.objects.count() + + # Create a unique set of manufacturer names + for idx, part in enumerate(SupplierPart.objects.all()): + + if part.manufacturer is not None: + print(" -> Part '{p}' already has a manufacturer associated (skipping)".format(p=part)) + continue + + map_part_to_manufacturer(part, idx, part_count) + parts.append(part) + + print("Done!") + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0018_supplierpart_manufacturer'), + ] + + operations = [ + migrations.RunPython(associate_manufacturers, reverse_code=reverse_association) + ] diff --git a/InvenTree/company/migrations/0020_auto_20200413_0839.py b/InvenTree/company/migrations/0020_auto_20200413_0839.py new file mode 100644 index 0000000000..a3c7b9b64a --- /dev/null +++ b/InvenTree/company/migrations/0020_auto_20200413_0839.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.10 on 2020-04-13 08:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0019_auto_20200413_0642'), + ] + + operations = [ + migrations.AlterField( + model_name='supplierpart', + name='supplier', + field=models.ForeignKey(help_text='Select supplier', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='supplied_parts', to='company.Company'), + ), + ] diff --git a/InvenTree/company/migrations/0021_remove_supplierpart_manufacturer_name.py b/InvenTree/company/migrations/0021_remove_supplierpart_manufacturer_name.py new file mode 100644 index 0000000000..f90a1372b9 --- /dev/null +++ b/InvenTree/company/migrations/0021_remove_supplierpart_manufacturer_name.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.10 on 2020-04-13 10:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0020_auto_20200413_0839'), + ] + + operations = [ + migrations.RemoveField( + model_name='supplierpart', + name='manufacturer_name', + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 462b4ff847..3b8f058bfd 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -13,7 +13,7 @@ from decimal import Decimal from django.utils.translation import gettext_lazy as _ from django.core.validators import MinValueValidator from django.db import models -from django.db.models import Sum +from django.db.models import Sum, Q from django.apps import apps from django.urls import reverse @@ -23,6 +23,7 @@ from markdownx.models import MarkdownxField from stdimage.models import StdImageField from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail +from InvenTree.helpers import normalize from InvenTree.fields import InvenTreeURLField, RoundingDecimalField from InvenTree.status_codes import OrderStatus from common.models import Currency @@ -56,7 +57,12 @@ def rename_company_image(instance, filename): class Company(models.Model): """ A Company object represents an external company. - It may be a supplier or a customer (or both). + It may be a supplier or a customer or a manufacturer (or a combination) + + - A supplier is a company from which parts can be purchased + - A customer is a company to which parts can be sold + - A manufacturer is a company which manufactures a raw good (they may or may not be a "supplier" also) + Attributes: name: Brief name of the company @@ -70,6 +76,7 @@ class Company(models.Model): notes: Extra notes about the company is_customer: boolean value, is this company a customer is_supplier: boolean value, is this company a supplier + is_manufacturer: boolean value, is this company a manufacturer """ name = models.CharField(max_length=100, blank=False, unique=True, @@ -106,6 +113,8 @@ class Company(models.Model): is_supplier = models.BooleanField(default=True, help_text=_('Do you purchase items from this company?')) + is_manufacturer = models.BooleanField(default=False, help_text=_('Does this company manufacture parts?')) + def __str__(self): """ Get string representation of a Company """ return "{n} - {d}".format(n=self.name, d=self.description) @@ -131,26 +140,48 @@ class Company(models.Model): return getBlankThumbnail() @property - def part_count(self): + def manufactured_part_count(self): + """ The number of parts manufactured by this company """ + return self.manufactured_parts.count() + + @property + def has_manufactured_parts(self): + return self.manufactured_part_count > 0 + + @property + def supplied_part_count(self): """ The number of parts supplied by this company """ + return self.supplied_parts.count() + + @property + def has_supplied_parts(self): + """ Return True if this company supplies any parts """ + return self.supplied_part_count > 0 + + @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)) + + @property + def part_count(self): + """ The number of parts manufactured (or supplied) by this Company """ return self.parts.count() @property def has_parts(self): - """ Return True if this company supplies any parts """ return self.part_count > 0 @property def stock_items(self): - """ Return a list of all stock items supplied by this company """ + """ Return a list of all stock items supplied or manufactured by this company """ stock = apps.get_model('stock', 'StockItem') - return stock.objects.filter(supplier_part__supplier=self.id).all() + return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer=self.id)).all() @property def stock_count(self): - """ Return the number of stock items supplied by this company """ - stock = apps.get_model('stock', 'StockItem') - return stock.objects.filter(supplier_part__supplier=self.id).count() + """ Return the number of stock items supplied or manufactured by this company """ + return self.stock_items.count() def outstanding_purchase_orders(self): """ Return purchase orders which are 'outstanding' """ @@ -216,7 +247,7 @@ class SupplierPart(models.Model): part: Link to the master Part supplier: Company that supplies this SupplierPart object SKU: Stock keeping unit (supplier part number) - manufacturer: Manufacturer name + 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 description: Descriptive notes field @@ -246,14 +277,21 @@ class SupplierPart(models.Model): ) supplier = models.ForeignKey(Company, on_delete=models.CASCADE, - related_name='parts', + related_name='supplied_parts', limit_choices_to={'is_supplier': True}, help_text=_('Select supplier'), ) SKU = models.CharField(max_length=100, help_text=_('Supplier stock keeping unit')) - manufacturer = models.CharField(max_length=100, blank=True, help_text=_('Manufacturer')) + manufacturer = models.ForeignKey( + Company, + on_delete=models.SET_NULL, + related_name='manufactured_parts', + limit_choices_to={'is_manufacturer': True}, + help_text=_('Select manufacturer'), + null=True, blank=True + ) MPN = models.CharField(max_length=100, blank=True, help_text=_('Manufacturer part number')) @@ -281,7 +319,7 @@ class SupplierPart(models.Model): items = [] if self.manufacturer: - items.append(self.manufacturer) + items.append(self.manufacturer.name) if self.MPN: items.append(self.MPN) @@ -337,7 +375,7 @@ class SupplierPart(models.Model): if pb_found: cost = pb_cost * quantity - return cost + self.base_cost + return normalize(cost + self.base_cost) else: return None diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 935712a180..701c1faabf 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -17,12 +17,16 @@ class CompanyBriefSerializer(InvenTreeModelSerializer): url = serializers.CharField(source='get_absolute_url', read_only=True) + image = serializers.CharField(source='get_thumbnail_url', read_only=True) + class Meta: model = Company fields = [ 'pk', 'url', - 'name' + 'name', + 'description', + 'image', ] @@ -49,9 +53,10 @@ class CompanySerializer(InvenTreeModelSerializer): 'contact', 'link', 'image', - 'notes', 'is_customer', + 'is_manufacturer', 'is_supplier', + 'notes', 'part_count' ] @@ -63,20 +68,28 @@ class SupplierPartSerializer(InvenTreeModelSerializer): part_detail = PartBriefSerializer(source='part', many=False, read_only=True) - supplier_name = serializers.CharField(source='supplier.name', read_only=True) - supplier_logo = serializers.CharField(source='supplier.get_thumbnail_url', read_only=True) + supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True) + manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True) pricing = serializers.CharField(source='unit_pricing', read_only=True) def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) + supplier_detail = kwargs.pop('supplier_detail', False) + manufacturer_detail = kwargs.pop('manufacturer_detail', False) super(SupplierPartSerializer, self).__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') + if supplier_detail is not True: + self.fields.pop('supplier_detail') + + if manufacturer_detail is not True: + self.fields.pop('manufacturer_detail') + class Meta: model = SupplierPart fields = [ @@ -85,10 +98,10 @@ class SupplierPartSerializer(InvenTreeModelSerializer): 'part', 'part_detail', 'supplier', - 'supplier_name', - 'supplier_logo', + 'supplier_detail', 'SKU', 'manufacturer', + 'manufacturer_detail', 'description', 'MPN', 'link', diff --git a/InvenTree/company/templates/company/delete.html b/InvenTree/company/templates/company/delete.html index 9a94404739..191e07c2f3 100644 --- a/InvenTree/company/templates/company/delete.html +++ b/InvenTree/company/templates/company/delete.html @@ -6,8 +6,8 @@ Are you sure you want to delete company '{{ company.name }}'?
-{% if company.part_count > 0 %} -

There are {{ company.part_count }} parts sourced from this company.
+{% if company.supplied_part_count > 0 %} +

There are {{ company.supplied_part_count }} parts sourced from this company.
If this supplier is deleted, these supplier part entries will also be deleted.

    {% for part in company.parts.all %} diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index adb999fb4d..a166de9048 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -12,15 +12,20 @@ - - {% trans "Customer" %} - {% include 'yesnolabel.html' with value=company.is_customer %} + + {% trans "Manufacturer" %} + {% include "yesnolabel.html" with value=company.is_manufacturer %} - + {% trans "Supplier" %} {% include 'yesnolabel.html' with value=company.is_supplier %} + + + {% trans "Customer" %} + {% include 'yesnolabel.html' with value=company.is_customer %} + {% endblock %} diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index 46ec09ffc6..537f7b07c3 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -47,73 +47,18 @@ }); }); - $("#part-table").inventreeTable({ - formatNoMatches: function() { return "No supplier parts found for {{ company.name }}"; }, - queryParams: function(p) { - return { - supplier: {{ company.id }}, + loadSupplierPartTable( + "#part-table", + "{% url 'api-supplier-part-list' %}", + { + params: { part_detail: true, - } - }, - columns: [ - { - checkbox: true, + supplier_detail: true, + manufacturer_detail: true, + company: {{ company.id }}, }, - { - sortable: true, - field: 'part_detail.full_name', - title: '{% trans "Part" %}', - formatter: function(value, row, index, field) { - - var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, '/part/' + row.part + '/suppliers/'); - - if (row.part_detail.is_template) { - html += ``; - } - - if (row.part_detail.assembly) { - html += ``; - } - - if (!row.part_detail.active) { - html += `INACTIVE`; - } - - return html; - } - }, - { - sortable: true, - field: 'SKU', - title: '{% trans "SKU" %}', - formatter: function(value, row, index, field) { - return renderLink(value, row.url); - } - }, - { - sortable: true, - field: 'manufacturer', - title: '{% trans "Manufacturer" %}', - }, - { - sortable: true, - field: 'MPN', - title: 'MPN', - }, - { - field: 'link', - title: '{% trans "Link" %}', - formatter: function(value, row, index, field) { - if (value) { - return renderLink(value, value); - } else { - return ''; - } - } - }, - ], - url: "{% url 'api-part-supplier-list' %}" - }); + } + ); $("#multi-part-delete").click(function() { var selections = $("#part-table").bootstrapTable("getSelections"); diff --git a/InvenTree/company/templates/company/detail_purchase_orders.html b/InvenTree/company/templates/company/detail_purchase_orders.html index 7113297b0c..c83bb90eb1 100644 --- a/InvenTree/company/templates/company/detail_purchase_orders.html +++ b/InvenTree/company/templates/company/detail_purchase_orders.html @@ -25,7 +25,7 @@ {% block js_ready %} {{ block.super }} - loadPurchaseOrderTable($("#purchase-order-table"), { + loadPurchaseOrderTable("#purchase-order-table", { url: "{% url 'api-po-list' %}?supplier={{ company.id }}", }); @@ -48,7 +48,4 @@ newOrder(); }); - $(".po-table").inventreeTable({ - }); - {% endblock %} diff --git a/InvenTree/company/templates/company/detail_stock.html b/InvenTree/company/templates/company/detail_stock.html index 4acf783729..c33179d454 100644 --- a/InvenTree/company/templates/company/detail_stock.html +++ b/InvenTree/company/templates/company/detail_stock.html @@ -19,8 +19,9 @@ loadStockTable($('#stock-table'), { url: "{% url 'api-stock-list' %}", params: { - supplier: {{ company.id }}, + company: {{ company.id }}, part_detail: true, + supplier_detail: true, location_detail: true, }, buttons: [ diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 0c5e3a6931..f337019409 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -9,12 +9,12 @@ InvenTree | {% trans "Supplier List" %} {% block content %} -

    {% trans "Supplier List" %}

    +

    {{ title }}


    - +
    @@ -26,54 +26,17 @@ InvenTree | {% trans "Supplier List" %} {% block js_ready %} {{ block.super }} $('#new-company').click(function () { - launchModalForm( - "{% url 'company-create' %}", - { - follow: true - }); + launchModalForm("{{ create_url }}", { + follow: true + }); }); - $("#company-table").inventreeTable({ - formatNoMatches: function() { return "No company information found"; }, - columns: [ - { - field: 'pk', - title: '{% trans "ID" %}', - visible: false, - }, - { - field: 'name', - title: '{% trans "Supplier" %}', - sortable: true, - formatter: function(value, row, index, field) { - return imageHoverIcon(row.image) + renderLink(value, row.url); - } - }, - { - field: 'description', - title: '{% trans "Description" %}', - sortable: true, - }, - { - field: 'website', - title: '{% trans "Website" %}', - formatter: function(value, row, index, field) { - if (value) { - return renderLink(value, value); - } - return ''; - } - }, - { - field: 'part_count', - title: '{% trans "Parts" %}', - sortable: true, - formatter: function(value, row, index, field) { - return renderLink(value, row.url + 'parts/'); - } - }, - ], - url: "{% url 'api-company-list' %}" - }); + loadCompanyTable("#company-table", "{% url 'api-company-list' %}", + { + params: { + {% for key,value in filters.items %}{{ key }}: "{{ value }}",{% endfor %} + } + } + ); {% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/supplier_part_base.html b/InvenTree/company/templates/company/supplier_part_base.html index 33157ed67f..964c61ee5e 100644 --- a/InvenTree/company/templates/company/supplier_part_base.html +++ b/InvenTree/company/templates/company/supplier_part_base.html @@ -33,7 +33,9 @@ InvenTree | {% trans "Supplier Part" %}

    {% trans "Supplier Part Details" %}

    + + - - - {% if part.link %} - - {% endif %} - {% if part.description %} - - {% endif %} - {% if part.manufacturer %} - - - {% endif %} - {% if part.note %} - - {% endif %} + {% if part.description %} + + + + + {% endif %} + {% if part.link %} + + + + + + {% endif %} + + + + + + + + + + {% if part.manufacturer %} + + + + + + + + + + {% endif %} + {% if part.note %} + + + + + + {% endif %}
    {% trans "Internal Part" %} {% if part.part %} @@ -41,21 +43,46 @@ InvenTree | {% trans "Supplier Part" %} {% endif %}
    {% trans "Supplier" %}{{ part.supplier.name }}
    {% trans "SKU" %}{{ part.SKU }}
    {% trans "External Link" %}{{ part.link }}
    {% trans "Description" %}{{ part.description }}
    {% trans "Manufacturer" %}{{ part.manufacturer }}
    {% trans "MPN" %}{{ part.MPN }}
    {% trans "Note" %}{{ part.note }}
    {% trans "Description" %}{{ part.description }}
    {% trans "External Link" %}{{ part.link }}
    {% trans "Supplier" %}{{ part.supplier.name }}
    {% trans "SKU" %}{{ part.SKU }}
    {% trans "Manufacturer" %}{{ part.manufacturer.name }}
    {% trans "MPN" %}{{ part.MPN }}
    {% trans "Note" %}{{ part.note }}
    diff --git a/InvenTree/company/templates/company/tabs.html b/InvenTree/company/templates/company/tabs.html index 007d9e2e54..ea61c40574 100644 --- a/InvenTree/company/templates/company/tabs.html +++ b/InvenTree/company/templates/company/tabs.html @@ -4,13 +4,15 @@ {% trans "Details" %} - {% if company.is_supplier %} + {% if company.is_supplier or company.is_manufacturer %} - {% trans "Supplier Parts" %} {{ company.part_count }} + {% trans "Parts" %} {{ company.part_count }} {% trans "Stock" %} {{ company.stock_count }} + {% endif %} + {% if company.is_supplier %} {% trans "Purchase Orders" %} {{ company.purchase_orders.count }} diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index 58520689a1..ecc942f652 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -56,13 +56,13 @@ class CompanySimpleTest(TestCase): zerg = Company.objects.get(pk=3) self.assertTrue(acme.has_parts) - self.assertEqual(acme.part_count, 4) + self.assertEqual(acme.supplied_part_count, 4) self.assertTrue(appel.has_parts) - self.assertEqual(appel.part_count, 2) + self.assertEqual(appel.supplied_part_count, 2) self.assertTrue(zerg.has_parts) - self.assertEqual(zerg.part_count, 1) + self.assertEqual(zerg.supplied_part_count, 1) def test_price_breaks(self): diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 65eba73f84..3140b7c2d7 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -2,9 +2,7 @@ URL lookup for Company app """ - from django.conf.urls import url, include -from django.views.generic.base import RedirectView from . import views @@ -15,7 +13,7 @@ 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'parts/', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-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/detail_purchase_orders.html'), name='company-detail-purchase-orders'), url(r'notes/?', views.CompanyNotes.as_view(), name='company-notes'), @@ -29,14 +27,19 @@ company_detail_urls = [ company_urls = [ + url(r'new/supplier/', views.CompanyCreate.as_view(), name='supplier-create'), + url(r'new/manufacturer/', views.CompanyCreate.as_view(), name='manufacturer-create'), + url(r'new/customer/', views.CompanyCreate.as_view(), name='customer-create'), url(r'new/?', views.CompanyCreate.as_view(), name='company-create'), url(r'^(?P\d+)/', include(company_detail_urls)), - url(r'', views.CompanyIndex.as_view(), name='company-index'), + url(r'suppliers/', views.CompanyIndex.as_view(), name='supplier-index'), + url(r'manufacturers/', views.CompanyIndex.as_view(), name='manufacturer-index'), + url(r'customers/', views.CompanyIndex.as_view(), name='customer-index'), - # Redirect any other patterns - url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'), + # Redirect any other patterns to the 'company' index which displays all companies + url(r'^.*$', views.CompanyIndex.as_view(), name='company-index'), ] price_break_urls = [ diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 243a982872..d1fc9b643f 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -39,6 +39,56 @@ class CompanyIndex(ListView): context_object_name = 'companies' paginate_by = 50 + def get_context_data(self, **kwargs): + + ctx = super().get_context_data(**kwargs) + + # Provide custom context data to the template, + # based on the URL we use to access this page + + lookup = { + reverse('supplier-index'): { + 'title': _('Suppliers'), + 'button_text': _('New Supplier'), + 'filters': {'is_supplier': 'true'}, + 'create_url': reverse('supplier-create'), + }, + reverse('manufacturer-index'): { + 'title': _('Manufacturers'), + 'button_text': _('New Manufacturer'), + 'filters': {'is_manufacturer': 'true'}, + 'create_url': reverse('manufacturer-create'), + }, + reverse('customer-index'): { + 'title': _('Customers'), + 'button_text': _('New Customer'), + 'filters': {'is_customer': 'true'}, + 'create_url': reverse('customer-create'), + } + } + + default = { + 'title': _('Companies'), + 'button_text': _('New Company'), + 'filters': {}, + 'create_url': reverse('company-create'), + } + + context = None + + for item in lookup: + if self.request.path == item: + context = lookup[item] + break + + if context is None: + context = default + + for key, value in context.items(): + ctx[key] = value + + return ctx + def get_queryset(self): """ Retrieve the Company queryset based on HTTP request parameters. @@ -125,7 +175,44 @@ class CompanyCreate(AjaxCreateView): context_object_name = 'company' form_class = EditCompanyForm ajax_template_name = 'modal_form.html' - ajax_form_title = _("Create new Company") + + def get_form_title(self): + + url = self.request.path + + if url == reverse('supplier-create'): + return _("Create new Supplier") + + if url == reverse('manufacturer-create'): + return _('Create new Manufacturer') + + if url == reverse('customer-create'): + return _('Create new Customer') + + return _('Create new Company') + + def get_initial(self): + """ Initial values for the form data """ + initials = super().get_initial().copy() + + url = self.request.path + + if url == reverse('supplier-create'): + initials['is_supplier'] = True + initials['is_customer'] = False + initials['is_manufacturer'] = False + + elif url == reverse('manufacturer-create'): + initials['is_manufacturer'] = True + initials['is_supplier'] = True + initials['is_customer'] = False + + elif url == reverse('customer-create'): + initials['is_customer'] = True + initials['is_manufacturer'] = False + initials['is_supplier'] = False + + return initials def get_data(self): return { diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index d985b814df..54d57d2d8c 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -37,10 +37,7 @@ $("#po-create").click(function() { ); }); -$("#po-table").inventreeTable({ -}); - -loadPurchaseOrderTable($("#purchase-order-table"), { +loadPurchaseOrderTable("#purchase-order-table", { url: "{% url 'api-po-list' %}", }); diff --git a/InvenTree/part/templates/part/orders.html b/InvenTree/part/templates/part/orders.html index fbb0413e34..d53c6ca04f 100644 --- a/InvenTree/part/templates/part/orders.html +++ b/InvenTree/part/templates/part/orders.html @@ -9,8 +9,11 @@
    -
    +
    +
    + +
    diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html index 38596ba03c..e20baae3e9 100644 --- a/InvenTree/part/templates/part/supplier.html +++ b/InvenTree/part/templates/part/supplier.html @@ -66,58 +66,18 @@ }); }); - $("#supplier-table").inventreeTable({ - formatNoMatches: function() { return "No supplier parts available for {{ part.full_name }}"; }, - queryParams: function(p) { - return { - part: {{ part.id }} - } - }, - columns: [ - { - checkbox: true, + loadSupplierPartTable( + "#supplier-table", + "{% url 'api-supplier-part-list' %}", + { + params: { + part: {{ part.id }}, + part_detail: true, + supplier_detail: true, + manufacturer_detail: true, }, - { - sortable: true, - field: 'supplier_name', - title: 'Supplier', - formatter: function(value, row, index, field) { - return imageHoverIcon(row.supplier_logo) + renderLink(value, '/company/' + row.supplier + '/'); - } - }, - { - sortable: true, - field: 'SKU', - title: 'SKU', - formatter: function(value, row, index, field) { - return renderLink(value, row.url); - } - }, - { - sortable: true, - field: 'manufacturer', - title: 'Manufacturer', - }, - { - sortable: true, - field: 'MPN', - title: 'MPN', - }, - { - sortable: true, - field: 'pricing', - title: 'Price', - formatter: function(value, row, index, field) { - if (value) { - return value; - } else { - return "No pricing available"; - } - }, - } - ], - url: "{% url 'api-part-supplier-list' %}" - }); + } + ); linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 8f1311b96a..b36d097185 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1268,8 +1268,6 @@ class PartExport(AjaxView): # Filter by part category cat_id = request.GET.get('category', None) - print('cat_id:', cat_id) - part_list = None if cat_id is not None: diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index ebc5454d5b..b9132ab557 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -8,6 +8,7 @@ from django_filters import NumberFilter from django.conf import settings from django.conf.urls import url, include from django.urls import reverse +from django.db.models import Q from .models import StockLocation, StockItem from .models import StockItemTracking @@ -494,11 +495,23 @@ class StockList(generics.ListCreateAPIView): if supplier_part_id: stock_list = stock_list.filter(supplier_part=supplier_part_id) - # Filter by supplier ID - supplier_id = self.request.query_params.get('supplier', None) + # Filter by company (either manufacturer or supplier) + company = self.request.query_params.get('company', None) - if supplier_id: - stock_list = stock_list.filter(supplier_part__supplier=supplier_id) + if company is not None: + stock_list = stock_list.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer=company)) + + # Filter by supplier + supplier = self.request.query_params.get('supplier', None) + + if supplier is not None: + stock_list = stock_list.filter(supplier_part__supplier=supplier) + + # Filter by manufacturer + manufacturer = self.request.query_params.get('manufacturer', None) + + if manufacturer is not None: + stock_list = stock_list.filter(supplier_part__manufacturer=manufacturer) # Also ensure that we pre-fecth all the related items stock_list = stock_list.prefetch_related( diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index 03b64419c7..e8cda96809 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -135,55 +135,23 @@ InvenTree | {% trans "Search Results" %} } ); - $("#company-results-table").inventreeTable({ - url: "{% url 'api-company-list' %}", - queryParams: { - search: "{{ query }}", - }, - columns: [ - { - field: 'name', - title: 'Name', - formatter: function(value, row, index, field) { - return imageHoverIcon(row.image) + renderLink(value, row.url); - }, - }, - { - field: 'description', - title: 'Description', - }, - ] + loadCompanyTable('#company-results-table', "{% url 'api-company-list' %}", { + params: { + serach: "{{ query }}", + } }); - $("#supplier-part-results-table").inventreeTable({ - url: "{% url 'api-part-supplier-list' %}", - queryParams: { - search: "{{ query }}", - }, - columns: [ - { - field: 'supplier_name', - title: 'Supplier', - formatter: function(value, row, index, field) { - return imageHoverIcon(row.supplier_logo) + renderLink(value, '/company/' + row.supplier + '/'); - } + loadSupplierPartTable( + "#supplier-part-results-table", + "{% url 'api-supplier-part-list' %}", + { + params: { + search: "{{ query }}", + part_detail: true, + supplier_detail: true, + manufacturer_detail: true }, - { - field: 'SKU', - title: 'SKU', - formatter: function(value, row, index, field) { - return renderLink(value, row.url); - } - }, - { - field: 'manufacturer', - title: 'Manufacturer', - }, - { - field: 'MPN', - title: 'MPN', - } - ] - }); + } + ); {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 987e1872f2..3cae9fd37b 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -108,6 +108,7 @@ InvenTree + diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index e44107e2d1..c41ef3718f 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -10,8 +10,20 @@
  • {% trans "Parts" %}
  • {% trans "Stock" %}
  • {% trans "Build" %}
  • -
  • {% trans "Suppliers" %}
  • -
  • {% trans "Orders" %}
  • + +