Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-04-13 20:43:00 +10:00
commit 2e1c4e9792
37 changed files with 942 additions and 311 deletions

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ var/
local_settings.py local_settings.py
*.sqlite3 *.sqlite3
*.backup *.backup
*.old
# Sphinx files # Sphinx files
docs/_build docs/_build

View File

@ -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 += `<span title='Customer' class='fas fa-user-tie label-right'></span>`;
}
if (row.is_manufacturer) {
html += `<span title='Manufacturer' class='fas fa-industry label-right'></span>`;
}
if (row.is_supplier) {
html += `<span title='Supplier' class='fas fa-building label-right'></span>`;
}
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 += `<span class='fas fa-clone label-right' title='Template part'></span>`;
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools label-right' title='Assembled part'></span>`;
}
if (!row.part_detail.active) {
html += `<span class='label label-warning label-right'>INACTIVE</span>`;
}
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 '';
}
}
},
],
});
}

View File

@ -17,6 +17,7 @@ function defaultFilters() {
stock: "cascade=1", stock: "cascade=1",
build: "", build: "",
parts: "cascade=1", parts: "cascade=1",
company: "",
}; };
} }
@ -72,8 +73,6 @@ function saveTableFilters(tableKey, filters) {
var filterstring = strings.join('&'); var filterstring = strings.join('&');
console.log(`Saving filters for table '${tableKey}' - ${filterstring}`);
inventreeSave(lookup, filterstring); inventreeSave(lookup, filterstring);
} }
@ -255,12 +254,8 @@ function setupFilterList(tableKey, table, target) {
var clear = `filter-clear-${tableKey}`; var clear = `filter-clear-${tableKey}`;
var make = `filter-make-${tableKey}`; var make = `filter-make-${tableKey}`;
console.log(`Generating filter list: ${tableKey}`);
var filters = loadTableFilters(tableKey); var filters = loadTableFilters(tableKey);
console.log("Filters: " + filters.count);
var element = $(target); var element = $(target);
// One blank slate, please // One blank slate, please

View File

@ -114,7 +114,7 @@ function loadPurchaseOrderTable(table, options) {
setupFilterList("order", table); setupFilterList("order", table);
table.inventreeTable({ $(table).inventreeTable({
url: options.url, url: options.url,
queryParams: filters, queryParams: filters,
groupBy: false, groupBy: false,

View File

@ -228,8 +228,16 @@ function loadStockTable(table, options) {
name += " | "; name += " | ";
name += row.part__revision; 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);
} }
}, },
{ {

View File

@ -15,8 +15,6 @@ class StatusCode:
Render the value as a label. Render the value as a label.
""" """
print("Rendering:", key, cls.options)
# If the key cannot be found, pass it back # If the key cannot be found, pass it back
if key not in cls.options.keys(): if key not in cls.options.keys():
return key return key

View File

@ -115,9 +115,12 @@ class AjaxMixin(object):
# (this can be overridden by a child class) # (this can be overridden by a child class)
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_action = ''
ajax_form_title = '' 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'): def get_param(self, name, method='GET'):
""" Get a request query parameter value from URL e.g. ?part=3 """ Get a request query parameter value from URL e.g. ?part=3
@ -169,7 +172,7 @@ class AjaxMixin(object):
else: else:
context['form'] = None context['form'] = None
data['title'] = self.ajax_form_title data['title'] = self.get_form_title()
data['html_form'] = render_to_string( data['html_form'] = render_to_string(
self.ajax_template_name, self.ajax_template_name,

View File

@ -10,6 +10,7 @@ from rest_framework import filters
from rest_framework import generics, permissions from rest_framework import generics, permissions
from django.conf.urls import url, include from django.conf.urls import url, include
from django.db.models import Q
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
@ -43,9 +44,10 @@ class CompanyList(generics.ListCreateAPIView):
] ]
filter_fields = [ filter_fields = [
'name',
'is_customer', 'is_customer',
'is_manufacturer',
'is_supplier', 'is_supplier',
'name',
] ]
search_fields = [ search_fields = [
@ -80,22 +82,40 @@ class SupplierPartList(generics.ListCreateAPIView):
queryset = SupplierPart.objects.all().prefetch_related( queryset = SupplierPart.objects.all().prefetch_related(
'part', 'part',
'part__category',
'part__stock_items',
'part__bom_items',
'part__builds',
'supplier', '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): def get_serializer(self, *args, **kwargs):
# Do we wish to include extra detail? # Do we wish to include extra detail?
try: 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: 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() kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
@ -114,13 +134,14 @@ class SupplierPartList(generics.ListCreateAPIView):
filter_fields = [ filter_fields = [
'part', 'part',
'supplier' 'supplier',
'manufacturer',
] ]
search_fields = [ search_fields = [
'SKU', 'SKU',
'supplier__name', 'supplier__name',
'manufacturer', 'manufacturer__name',
'description', 'description',
'MPN', 'MPN',
] ]
@ -170,15 +191,15 @@ supplier_part_api_urls = [
url(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'), url(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
# Catch anything else # 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 = [ 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<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'), url(r'^(?P<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),

View File

@ -26,8 +26,9 @@ class EditCompanyForm(HelperForm):
'phone', 'phone',
'email', 'email',
'contact', 'contact',
'is_customer',
'is_supplier', 'is_supplier',
'is_manufacturer',
'is_customer',
] ]
@ -58,7 +59,6 @@ class EditSupplierPartForm(HelperForm):
'base_cost', 'base_cost',
'multiple', 'multiple',
'packaging', 'packaging',
# 'lead_time'
] ]

View File

@ -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?'),
),
]

View File

@ -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?'),
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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 <ENTER> 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)
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -13,7 +13,7 @@ from decimal import Decimal
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models 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.apps import apps
from django.urls import reverse from django.urls import reverse
@ -23,6 +23,7 @@ from markdownx.models import MarkdownxField
from stdimage.models import StdImageField from stdimage.models import StdImageField
from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail
from InvenTree.helpers import normalize
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
from InvenTree.status_codes import OrderStatus from InvenTree.status_codes import OrderStatus
from common.models import Currency from common.models import Currency
@ -56,7 +57,12 @@ def rename_company_image(instance, filename):
class Company(models.Model): class Company(models.Model):
""" A Company object represents an external company. """ 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: Attributes:
name: Brief name of the company name: Brief name of the company
@ -70,6 +76,7 @@ class Company(models.Model):
notes: Extra notes about the company notes: Extra notes about the company
is_customer: boolean value, is this company a customer is_customer: boolean value, is this company a customer
is_supplier: boolean value, is this company a supplier 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, 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_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): def __str__(self):
""" Get string representation of a Company """ """ Get string representation of a Company """
return "{n} - {d}".format(n=self.name, d=self.description) return "{n} - {d}".format(n=self.name, d=self.description)
@ -131,26 +140,48 @@ class Company(models.Model):
return getBlankThumbnail() return getBlankThumbnail()
@property @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 """ """ 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() return self.parts.count()
@property @property
def has_parts(self): def has_parts(self):
""" Return True if this company supplies any parts """
return self.part_count > 0 return self.part_count > 0
@property @property
def stock_items(self): 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') 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 @property
def stock_count(self): def stock_count(self):
""" Return the number of stock items supplied by this company """ """ Return the number of stock items supplied or manufactured by this company """
stock = apps.get_model('stock', 'StockItem') return self.stock_items.count()
return stock.objects.filter(supplier_part__supplier=self.id).count()
def outstanding_purchase_orders(self): def outstanding_purchase_orders(self):
""" Return purchase orders which are 'outstanding' """ """ Return purchase orders which are 'outstanding' """
@ -216,7 +247,7 @@ class SupplierPart(models.Model):
part: Link to the master Part part: Link to the master Part
supplier: Company that supplies this SupplierPart object supplier: Company that supplies this SupplierPart object
SKU: Stock keeping unit (supplier part number) SKU: Stock keeping unit (supplier part number)
manufacturer: Manufacturer name manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!)
MPN: Manufacture part number MPN: Manufacture part number
link: Link to external website for this part link: Link to external website for this part
description: Descriptive notes field description: Descriptive notes field
@ -246,14 +277,21 @@ class SupplierPart(models.Model):
) )
supplier = models.ForeignKey(Company, on_delete=models.CASCADE, supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
related_name='parts', related_name='supplied_parts',
limit_choices_to={'is_supplier': True}, limit_choices_to={'is_supplier': True},
help_text=_('Select supplier'), help_text=_('Select supplier'),
) )
SKU = models.CharField(max_length=100, help_text=_('Supplier stock keeping unit')) 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')) MPN = models.CharField(max_length=100, blank=True, help_text=_('Manufacturer part number'))
@ -281,7 +319,7 @@ class SupplierPart(models.Model):
items = [] items = []
if self.manufacturer: if self.manufacturer:
items.append(self.manufacturer) items.append(self.manufacturer.name)
if self.MPN: if self.MPN:
items.append(self.MPN) items.append(self.MPN)
@ -337,7 +375,7 @@ class SupplierPart(models.Model):
if pb_found: if pb_found:
cost = pb_cost * quantity cost = pb_cost * quantity
return cost + self.base_cost return normalize(cost + self.base_cost)
else: else:
return None return None

View File

@ -17,12 +17,16 @@ class CompanyBriefSerializer(InvenTreeModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
image = serializers.CharField(source='get_thumbnail_url', read_only=True)
class Meta: class Meta:
model = Company model = Company
fields = [ fields = [
'pk', 'pk',
'url', 'url',
'name' 'name',
'description',
'image',
] ]
@ -49,9 +53,10 @@ class CompanySerializer(InvenTreeModelSerializer):
'contact', 'contact',
'link', 'link',
'image', 'image',
'notes',
'is_customer', 'is_customer',
'is_manufacturer',
'is_supplier', 'is_supplier',
'notes',
'part_count' 'part_count'
] ]
@ -63,20 +68,28 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
supplier_name = serializers.CharField(source='supplier.name', read_only=True) supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
supplier_logo = serializers.CharField(source='supplier.get_thumbnail_url', read_only=True) manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True)
pricing = serializers.CharField(source='unit_pricing', read_only=True) pricing = serializers.CharField(source='unit_pricing', read_only=True)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False) 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) super(SupplierPartSerializer, self).__init__(*args, **kwargs)
if part_detail is not True: if part_detail is not True:
self.fields.pop('part_detail') 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: class Meta:
model = SupplierPart model = SupplierPart
fields = [ fields = [
@ -85,10 +98,10 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
'part', 'part',
'part_detail', 'part_detail',
'supplier', 'supplier',
'supplier_name', 'supplier_detail',
'supplier_logo',
'SKU', 'SKU',
'manufacturer', 'manufacturer',
'manufacturer_detail',
'description', 'description',
'MPN', 'MPN',
'link', 'link',

View File

@ -6,8 +6,8 @@ Are you sure you want to delete company '{{ company.name }}'?
<br> <br>
{% if company.part_count > 0 %} {% if company.supplied_part_count > 0 %}
<p>There are {{ company.part_count }} parts sourced from this company.<br> <p>There are {{ company.supplied_part_count }} parts sourced from this company.<br>
If this supplier is deleted, these supplier part entries will also be deleted.</p> If this supplier is deleted, these supplier part entries will also be deleted.</p>
<ul class='list-group'> <ul class='list-group'>
{% for part in company.parts.all %} {% for part in company.parts.all %}

View File

@ -12,15 +12,20 @@
<col width='25'> <col width='25'>
<col> <col>
<tr> <tr>
<td><span class='fas fa-user-tag'></span></td> <td><span class='fas fa-industry'></span></td>
<td>{% trans "Customer" %}</td> <td>{% trans "Manufacturer" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td> <td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
</tr> </tr>
<tr> <tr>
<td><span class='fas fa-industry'></span></td> <td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td> <td>{% trans "Supplier" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td> <td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
</tr> </tr>
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
</tr>
</table> </table>
{% endblock %} {% endblock %}

View File

@ -47,73 +47,18 @@
}); });
}); });
$("#part-table").inventreeTable({ loadSupplierPartTable(
formatNoMatches: function() { return "No supplier parts found for {{ company.name }}"; }, "#part-table",
queryParams: function(p) { "{% url 'api-supplier-part-list' %}",
return { {
supplier: {{ company.id }}, params: {
part_detail: true, part_detail: true,
} supplier_detail: true,
}, manufacturer_detail: true,
columns: [ company: {{ company.id }},
{
checkbox: true,
}, },
{ }
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 += `<span class='fas fa-clone label-right' title='Template part'></span>`;
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools label-right' title='Assembled part'></span>`;
}
if (!row.part_detail.active) {
html += `<span class='label label-warning label-right'>INACTIVE</span>`;
}
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() { $("#multi-part-delete").click(function() {
var selections = $("#part-table").bootstrapTable("getSelections"); var selections = $("#part-table").bootstrapTable("getSelections");

View File

@ -25,7 +25,7 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
loadPurchaseOrderTable($("#purchase-order-table"), { loadPurchaseOrderTable("#purchase-order-table", {
url: "{% url 'api-po-list' %}?supplier={{ company.id }}", url: "{% url 'api-po-list' %}?supplier={{ company.id }}",
}); });
@ -48,7 +48,4 @@
newOrder(); newOrder();
}); });
$(".po-table").inventreeTable({
});
{% endblock %} {% endblock %}

View File

@ -19,8 +19,9 @@
loadStockTable($('#stock-table'), { loadStockTable($('#stock-table'), {
url: "{% url 'api-stock-list' %}", url: "{% url 'api-stock-list' %}",
params: { params: {
supplier: {{ company.id }}, company: {{ company.id }},
part_detail: true, part_detail: true,
supplier_detail: true,
location_detail: true, location_detail: true,
}, },
buttons: [ buttons: [

View File

@ -9,12 +9,12 @@ InvenTree | {% trans "Supplier List" %}
{% block content %} {% block content %}
<h3>{% trans "Supplier List" %}</h3> <h3>{{ title }}</h3>
<hr> <hr>
<div id='button-toolbar'> <div id='button-toolbar'>
<div class='btn-group'> <div class='btn-group'>
<button type='button' class="btn btn-success" id='new-company' title='Add new supplier'>{% trans "New Supplier" %}</button> <button type='button' class="btn btn-success" id='new-company'>{{ button_text }}</button>
</div> </div>
</div> </div>
@ -26,54 +26,17 @@ InvenTree | {% trans "Supplier List" %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
$('#new-company').click(function () { $('#new-company').click(function () {
launchModalForm( launchModalForm("{{ create_url }}", {
"{% url 'company-create' %}", follow: true
{ });
follow: true
});
}); });
$("#company-table").inventreeTable({ loadCompanyTable("#company-table", "{% url 'api-company-list' %}",
formatNoMatches: function() { return "No company information found"; }, {
columns: [ params: {
{ {% for key,value in filters.items %}{{ key }}: "{{ value }}",{% endfor %}
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' %}"
});
{% endblock %} {% endblock %}

View File

@ -33,7 +33,9 @@ InvenTree | {% trans "Supplier Part" %}
<div class='col-sm-6'> <div class='col-sm-6'>
<h4>{% trans "Supplier Part Details" %}</h4> <h4>{% trans "Supplier Part Details" %}</h4>
<table class="table table-striped table-condensed"> <table class="table table-striped table-condensed">
<col width='25'>
<tr> <tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Internal Part" %}</td> <td>{% trans "Internal Part" %}</td>
<td> <td>
{% if part.part %} {% if part.part %}
@ -41,21 +43,46 @@ InvenTree | {% trans "Supplier Part" %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr><td>{% trans "Supplier" %}</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr> {% if part.description %}
<tr><td>{% trans "SKU" %}</td><td>{{ part.SKU }}</tr></tr> <tr>
{% if part.link %} <td>{% trans "Description" %}</td>
<tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr> <td>{{ part.description }}</td>
{% endif %} </tr>
{% if part.description %} {% endif %}
<tr><td>{% trans "Description" %}</td><td>{{ part.description }}</td></tr> {% if part.link %}
{% endif %} <tr>
{% if part.manufacturer %} <td><span class='fas fa-link'></span></td>
<tr><td>{% trans "Manufacturer" %}</td><td>{{ part.manufacturer }}</td></tr> <td>{% trans "External Link" %}</td>
<tr><td>{% trans "MPN" %}</td><td>{{ part.MPN }}</td></tr> <td><a href="{{ part.link }}">{{ part.link }}</a></td>
{% endif %} </tr>
{% if part.note %} {% endif %}
<tr><td>{% trans "Note" %}</td><td>{{ part.note }}</td></tr> <tr>
{% endif %} <td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<tr>
<td></td>
<td>{% trans "SKU" %}</td>
<td>{{ part.SKU }}</tr>
</tr>
{% if part.manufacturer %}
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{% trans "Manufacturer" %}</td>
<td><a href="{% url 'company-detail-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a></td></tr>
<tr>
<td></td>
<td>{% trans "MPN" %}</td>
<td>{{ part.MPN }}</td>
</tr>
{% endif %}
{% if part.note %}
<tr>
<td></td>
<td>{% trans "Note" %}</td>
<td>{{ part.note }}</td>
</tr>
{% endif %}
</table> </table>
</div> </div>
</div> </div>

View File

@ -4,13 +4,15 @@
<li{% if tab == 'details' %} class='active'{% endif %}> <li{% if tab == 'details' %} class='active'{% endif %}>
<a href="{% url 'company-detail' company.id %}">{% trans "Details" %}</a> <a href="{% url 'company-detail' company.id %}">{% trans "Details" %}</a>
</li> </li>
{% if company.is_supplier %} {% if company.is_supplier or company.is_manufacturer %}
<li{% if tab == 'parts' %} class='active'{% endif %}> <li{% if tab == 'parts' %} class='active'{% endif %}>
<a href="{% url 'company-detail-parts' company.id %}">{% trans "Supplier Parts" %} <span class='badge'>{{ company.part_count }}</span></a> <a href="{% url 'company-detail-parts' company.id %}">{% trans "Parts" %} <span class='badge'>{{ company.part_count }}</span></a>
</li> </li>
<li{% if tab == 'stock' %} class='active'{% endif %}> <li{% if tab == 'stock' %} class='active'{% endif %}>
<a href="{% url 'company-detail-stock' company.id %}">{% trans "Stock" %} <span class='badge'>{{ company.stock_count }}</a> <a href="{% url 'company-detail-stock' company.id %}">{% trans "Stock" %} <span class='badge'>{{ company.stock_count }}</a>
</li> </li>
{% endif %}
{% if company.is_supplier %}
<li{% if tab == 'po' %} class='active'{% endif %}> <li{% if tab == 'po' %} class='active'{% endif %}>
<a href="{% url 'company-detail-purchase-orders' company.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ company.purchase_orders.count }}</span></a> <a href="{% url 'company-detail-purchase-orders' company.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ company.purchase_orders.count }}</span></a>
</li> </li>

View File

@ -56,13 +56,13 @@ class CompanySimpleTest(TestCase):
zerg = Company.objects.get(pk=3) zerg = Company.objects.get(pk=3)
self.assertTrue(acme.has_parts) self.assertTrue(acme.has_parts)
self.assertEqual(acme.part_count, 4) self.assertEqual(acme.supplied_part_count, 4)
self.assertTrue(appel.has_parts) self.assertTrue(appel.has_parts)
self.assertEqual(appel.part_count, 2) self.assertEqual(appel.supplied_part_count, 2)
self.assertTrue(zerg.has_parts) self.assertTrue(zerg.has_parts)
self.assertEqual(zerg.part_count, 1) self.assertEqual(zerg.supplied_part_count, 1)
def test_price_breaks(self): def test_price_breaks(self):

View File

@ -2,9 +2,7 @@
URL lookup for Company app URL lookup for Company app
""" """
from django.conf.urls import url, include from django.conf.urls import url, include
from django.views.generic.base import RedirectView
from . import views 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'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'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'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'), url(r'notes/?', views.CompanyNotes.as_view(), name='company-notes'),
@ -29,14 +27,19 @@ company_detail_urls = [
company_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'new/?', views.CompanyCreate.as_view(), name='company-create'),
url(r'^(?P<pk>\d+)/', include(company_detail_urls)), url(r'^(?P<pk>\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 # Redirect any other patterns to the 'company' index which displays all companies
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'), url(r'^.*$', views.CompanyIndex.as_view(), name='company-index'),
] ]
price_break_urls = [ price_break_urls = [

View File

@ -39,6 +39,56 @@ class CompanyIndex(ListView):
context_object_name = 'companies' context_object_name = 'companies'
paginate_by = 50 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): def get_queryset(self):
""" Retrieve the Company queryset based on HTTP request parameters. """ Retrieve the Company queryset based on HTTP request parameters.
@ -125,7 +175,44 @@ class CompanyCreate(AjaxCreateView):
context_object_name = 'company' context_object_name = 'company'
form_class = EditCompanyForm form_class = EditCompanyForm
ajax_template_name = 'modal_form.html' 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): def get_data(self):
return { return {

View File

@ -37,10 +37,7 @@ $("#po-create").click(function() {
); );
}); });
$("#po-table").inventreeTable({ loadPurchaseOrderTable("#purchase-order-table", {
});
loadPurchaseOrderTable($("#purchase-order-table"), {
url: "{% url 'api-po-list' %}", url: "{% url 'api-po-list' %}",
}); });

View File

@ -9,8 +9,11 @@
<hr> <hr>
<div id='button-bar'> <div id='button-bar'>
<div class='btn-group'> <div class='button-toolbar container-fluid' style='float: right;'>
<button class='btn btn-primary' type='button' id='part-order2' title='Order part'>Order Part</button> <button class='btn btn-primary' type='button' id='part-order2' title='Order part'>Order Part</button>
<div class='filter-list' id='filter-list-order'>
<!-- An empty div in which the filter list will be constructed -->
</div>
</div> </div>
</div> </div>

View File

@ -66,58 +66,18 @@
}); });
}); });
$("#supplier-table").inventreeTable({ loadSupplierPartTable(
formatNoMatches: function() { return "No supplier parts available for {{ part.full_name }}"; }, "#supplier-table",
queryParams: function(p) { "{% url 'api-supplier-part-list' %}",
return { {
part: {{ part.id }} params: {
} part: {{ part.id }},
}, part_detail: true,
columns: [ supplier_detail: true,
{ manufacturer_detail: true,
checkbox: 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 "<span class='warning-msg'><i>No pricing available</i></span>";
}
},
}
],
url: "{% url 'api-part-supplier-list' %}"
});
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']) linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])

View File

@ -1268,8 +1268,6 @@ class PartExport(AjaxView):
# Filter by part category # Filter by part category
cat_id = request.GET.get('category', None) cat_id = request.GET.get('category', None)
print('cat_id:', cat_id)
part_list = None part_list = None
if cat_id is not None: if cat_id is not None:

View File

@ -8,6 +8,7 @@ from django_filters import NumberFilter
from django.conf import settings from django.conf import settings
from django.conf.urls import url, include from django.conf.urls import url, include
from django.urls import reverse from django.urls import reverse
from django.db.models import Q
from .models import StockLocation, StockItem from .models import StockLocation, StockItem
from .models import StockItemTracking from .models import StockItemTracking
@ -494,11 +495,23 @@ class StockList(generics.ListCreateAPIView):
if supplier_part_id: if supplier_part_id:
stock_list = stock_list.filter(supplier_part=supplier_part_id) stock_list = stock_list.filter(supplier_part=supplier_part_id)
# Filter by supplier ID # Filter by company (either manufacturer or supplier)
supplier_id = self.request.query_params.get('supplier', None) company = self.request.query_params.get('company', None)
if supplier_id: if company is not None:
stock_list = stock_list.filter(supplier_part__supplier=supplier_id) 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 # Also ensure that we pre-fecth all the related items
stock_list = stock_list.prefetch_related( stock_list = stock_list.prefetch_related(

View File

@ -135,55 +135,23 @@ InvenTree | {% trans "Search Results" %}
} }
); );
$("#company-results-table").inventreeTable({ loadCompanyTable('#company-results-table', "{% url 'api-company-list' %}", {
url: "{% url 'api-company-list' %}", params: {
queryParams: { serach: "{{ query }}",
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',
},
]
}); });
$("#supplier-part-results-table").inventreeTable({ loadSupplierPartTable(
url: "{% url 'api-part-supplier-list' %}", "#supplier-part-results-table",
queryParams: { "{% url 'api-supplier-part-list' %}",
search: "{{ query }}", {
}, params: {
columns: [ search: "{{ query }}",
{ part_detail: true,
field: 'supplier_name', supplier_detail: true,
title: 'Supplier', manufacturer_detail: true
formatter: function(value, row, index, field) {
return imageHoverIcon(row.supplier_logo) + renderLink(value, '/company/' + row.supplier + '/');
}
}, },
{ }
field: 'SKU', );
title: 'SKU',
formatter: function(value, row, index, field) {
return renderLink(value, row.url);
}
},
{
field: 'manufacturer',
title: 'Manufacturer',
},
{
field: 'MPN',
title: 'MPN',
}
]
});
{% endblock %} {% endblock %}

View File

@ -108,6 +108,7 @@ InvenTree
<script type='text/javascript' src="{% static 'script/inventree/build.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/build.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/order.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/order.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/company.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>

View File

@ -10,8 +10,20 @@
<li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span> {% trans "Parts" %}</a></li> <li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span> {% trans "Parts" %}</a></li>
<li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li> <li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li>
<li><a href="{% url 'build-index' %}"><span class='fas fa-tools icon-header'></span>{% trans "Build" %}</a></li> <li><a href="{% url 'build-index' %}"><span class='fas fa-tools icon-header'></span>{% trans "Build" %}</a></li>
<li><a href="{% url 'company-index' %}"><span class='fas fa-industry icon-header'></span>{% trans "Suppliers" %}</a></li> <li class='nav navbar-nav'>
<li><a href="{% url 'po-index' %}"><span class='fas fa-shopping-cart icon-header'></span>{% trans "Orders" %}</a></li> <a class='dropdown-toggle' data-toggle='dropdown' href='#'><span class='fas fa-shopping-cart icon-header'></span>{% trans "Buy" %}</a>
<ul class='dropdown-menu'>
<li><a href="{% url 'supplier-index' %}"><span class='fas fa-building icon-header'></span>{% trans "Suppliers" %}</a></li>
<li><a href="{% url 'manufacturer-index' %}"><span class='fas fa-industry icon-header'></span>{% trans "Manufacturers" %}</a></li>
<li><a href="{% url 'po-index' %}"><span class='fas fa-list icon-header'></span>{% trans "Purchase Orders" %}</a></li>
</ul>
</li>
<li class='nav navbar-nav'>
<a class='dropdown-toggle' data-toggle='dropdown' href='#'><span class='fas fa-truck icon-header'></span>{% trans "Sell" %}</a>
<ul class='dropdown-menu'>
<li><a href="{% url 'customer-index' %}"><span class='fas fa-user-tie icon-header'></span>{% trans "Customers" %}</a>
</ul>
</li>
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% include "search_form.html" %} {% include "search_form.html" %}