mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
2e1c4e9792
1
.gitignore
vendored
1
.gitignore
vendored
@ -32,6 +32,7 @@ var/
|
||||
local_settings.py
|
||||
*.sqlite3
|
||||
*.backup
|
||||
*.old
|
||||
|
||||
# Sphinx files
|
||||
docs/_build
|
||||
|
188
InvenTree/InvenTree/static/script/inventree/company.js
Normal file
188
InvenTree/InvenTree/static/script/inventree/company.js
Normal 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 '';
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
@ -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
|
||||
|
@ -114,7 +114,7 @@ function loadPurchaseOrderTable(table, options) {
|
||||
|
||||
setupFilterList("order", table);
|
||||
|
||||
table.inventreeTable({
|
||||
$(table).inventreeTable({
|
||||
url: options.url,
|
||||
queryParams: filters,
|
||||
groupBy: false,
|
||||
|
@ -229,7 +229,15 @@ function loadStockTable(table, options) {
|
||||
name += row.part__revision;
|
||||
}
|
||||
|
||||
return imageHoverIcon(row.part__thumbnail) + renderLink(name, '/part/' + row.part + '/stock/');
|
||||
var url = '';
|
||||
|
||||
if (row.supplier_part) {
|
||||
url = `/supplier-part/${row.supplier_part}/`;
|
||||
} else {
|
||||
url = `/part/${row.part}/`;
|
||||
}
|
||||
|
||||
return imageHoverIcon(row.part__thumbnail) + renderLink(name, url);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
try:
|
||||
kwargs['manufacturer_detail'] = str2bool(self.request.query_params.get('manufacturer_detail', None))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
kwargs['part_detail'] = part_detail
|
||||
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<pk>\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<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),
|
||||
|
||||
|
@ -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'
|
||||
]
|
||||
|
||||
|
||||
|
18
InvenTree/company/migrations/0015_company_is_manufacturer.py
Normal file
18
InvenTree/company/migrations/0015_company_is_manufacturer.py
Normal 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?'),
|
||||
),
|
||||
]
|
18
InvenTree/company/migrations/0016_auto_20200412_2330.py
Normal file
18
InvenTree/company/migrations/0016_auto_20200412_2330.py
Normal 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?'),
|
||||
),
|
||||
]
|
18
InvenTree/company/migrations/0017_auto_20200413_0320.py
Normal file
18
InvenTree/company/migrations/0017_auto_20200413_0320.py
Normal 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',
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
275
InvenTree/company/migrations/0019_auto_20200413_0642.py
Normal file
275
InvenTree/company/migrations/0019_auto_20200413_0642.py
Normal 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)
|
||||
]
|
19
InvenTree/company/migrations/0020_auto_20200413_0839.py
Normal file
19
InvenTree/company/migrations/0020_auto_20200413_0839.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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',
|
||||
),
|
||||
]
|
@ -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
|
||||
|
||||
|
@ -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',
|
||||
|
@ -6,8 +6,8 @@ Are you sure you want to delete company '{{ company.name }}'?
|
||||
|
||||
<br>
|
||||
|
||||
{% if company.part_count > 0 %}
|
||||
<p>There are {{ company.part_count }} parts sourced from this company.<br>
|
||||
{% if company.supplied_part_count > 0 %}
|
||||
<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>
|
||||
<ul class='list-group'>
|
||||
{% for part in company.parts.all %}
|
||||
|
@ -12,15 +12,20 @@
|
||||
<col width='25'>
|
||||
<col>
|
||||
<tr>
|
||||
<td><span class='fas fa-user-tag'></span></td>
|
||||
<td>{% trans "Customer" %}</td>
|
||||
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Manufacturer" %}</td>
|
||||
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td><span class='fas fa-building'></span></td>
|
||||
<td>{% trans "Supplier" %}</td>
|
||||
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
|
||||
</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>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -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 += `<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() {
|
||||
var selections = $("#part-table").bootstrapTable("getSelections");
|
||||
|
@ -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 %}
|
||||
|
@ -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: [
|
||||
|
@ -9,12 +9,12 @@ InvenTree | {% trans "Supplier List" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{% trans "Supplier List" %}</h3>
|
||||
<h3>{{ title }}</h3>
|
||||
<hr>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<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>
|
||||
|
||||
@ -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 %}
|
@ -33,7 +33,9 @@ InvenTree | {% trans "Supplier Part" %}
|
||||
<div class='col-sm-6'>
|
||||
<h4>{% trans "Supplier Part Details" %}</h4>
|
||||
<table class="table table-striped table-condensed">
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Internal Part" %}</td>
|
||||
<td>
|
||||
{% if part.part %}
|
||||
@ -41,21 +43,46 @@ InvenTree | {% trans "Supplier Part" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>{% trans "Supplier" %}</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
|
||||
<tr><td>{% trans "SKU" %}</td><td>{{ part.SKU }}</tr></tr>
|
||||
{% if part.link %}
|
||||
<tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr>
|
||||
{% endif %}
|
||||
{% if part.description %}
|
||||
<tr><td>{% trans "Description" %}</td><td>{{ part.description }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.manufacturer %}
|
||||
<tr><td>{% trans "Manufacturer" %}</td><td>{{ part.manufacturer }}</td></tr>
|
||||
<tr><td>{% trans "MPN" %}</td><td>{{ part.MPN }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.note %}
|
||||
<tr><td>{% trans "Note" %}</td><td>{{ part.note }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.description %}
|
||||
<tr>
|
||||
<td>{% trans "Description" %}</td>
|
||||
<td>{{ part.description }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.link %}
|
||||
<tr>
|
||||
<td><span class='fas fa-link'></span></td>
|
||||
<td>{% trans "External Link" %}</td>
|
||||
<td><a href="{{ part.link }}">{{ part.link }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,13 +4,15 @@
|
||||
<li{% if tab == 'details' %} class='active'{% endif %}>
|
||||
<a href="{% url 'company-detail' company.id %}">{% trans "Details" %}</a>
|
||||
</li>
|
||||
{% if company.is_supplier %}
|
||||
{% if company.is_supplier or company.is_manufacturer %}
|
||||
<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{% if tab == 'stock' %} class='active'{% endif %}>
|
||||
<a href="{% url 'company-detail-stock' company.id %}">{% trans "Stock" %} <span class='badge'>{{ company.stock_count }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if company.is_supplier %}
|
||||
<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>
|
||||
</li>
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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<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
|
||||
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 = [
|
||||
|
@ -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 {
|
||||
|
@ -37,10 +37,7 @@ $("#po-create").click(function() {
|
||||
);
|
||||
});
|
||||
|
||||
$("#po-table").inventreeTable({
|
||||
});
|
||||
|
||||
loadPurchaseOrderTable($("#purchase-order-table"), {
|
||||
loadPurchaseOrderTable("#purchase-order-table", {
|
||||
url: "{% url 'api-po-list' %}",
|
||||
});
|
||||
|
||||
|
@ -9,8 +9,11 @@
|
||||
<hr>
|
||||
|
||||
<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>
|
||||
<div class='filter-list' id='filter-list-order'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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 "<span class='warning-msg'><i>No pricing available</i></span>";
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
url: "{% url 'api-part-supplier-list' %}"
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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 %}
|
@ -108,6 +108,7 @@ InvenTree
|
||||
<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/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/sidenav.js' %}"></script>
|
||||
|
||||
|
@ -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 '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 'company-index' %}"><span class='fas fa-industry icon-header'></span>{% trans "Suppliers" %}</a></li>
|
||||
<li><a href="{% url 'po-index' %}"><span class='fas fa-shopping-cart icon-header'></span>{% trans "Orders" %}</a></li>
|
||||
<li class='nav navbar-nav'>
|
||||
<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 class="nav navbar-nav navbar-right">
|
||||
{% include "search_form.html" %}
|
||||
|
Loading…
Reference in New Issue
Block a user