mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Merge remote-tracking branch 'inventree/master'
This commit is contained in:
		
							
								
								
									
										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,
 | 
			
		||||
 
 | 
			
		||||
@@ -228,8 +228,16 @@ function loadStockTable(table, options) {
 | 
			
		||||
                        name += " | ";
 | 
			
		||||
                        name += row.part__revision;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var url = '';
 | 
			
		||||
 | 
			
		||||
                    if (row.supplier_part) {
 | 
			
		||||
                        url = `/supplier-part/${row.supplier_part}/`;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        url = `/part/${row.part}/`;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    return imageHoverIcon(row.part__thumbnail) + renderLink(name, '/part/' + row.part + '/stock/');
 | 
			
		||||
                    return imageHoverIcon(row.part__thumbnail) + renderLink(name, url);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
        kwargs['part_detail'] = part_detail
 | 
			
		||||
        try:
 | 
			
		||||
            kwargs['manufacturer_detail'] = str2bool(self.request.query_params.get('manufacturer_detail', None))
 | 
			
		||||
        except AttributeError:
 | 
			
		||||
            pass
 | 
			
		||||
        
 | 
			
		||||
        kwargs['context'] = self.get_serializer_context()
 | 
			
		||||
 | 
			
		||||
        return self.serializer_class(*args, **kwargs)
 | 
			
		||||
@@ -114,13 +134,14 @@ class SupplierPartList(generics.ListCreateAPIView):
 | 
			
		||||
 | 
			
		||||
    filter_fields = [
 | 
			
		||||
        'part',
 | 
			
		||||
        'supplier'
 | 
			
		||||
        'supplier',
 | 
			
		||||
        'manufacturer',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        'SKU',
 | 
			
		||||
        'supplier__name',
 | 
			
		||||
        'manufacturer',
 | 
			
		||||
        'manufacturer__name',
 | 
			
		||||
        'description',
 | 
			
		||||
        'MPN',
 | 
			
		||||
    ]
 | 
			
		||||
@@ -170,15 +191,15 @@ supplier_part_api_urls = [
 | 
			
		||||
    url(r'^(?P<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" %}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user