mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Move SupplierPart and SupplierPriceBreak to the 'Company' app
- https://docs.djangoproject.com/en/2.2/ref/models/options/#django.db.models.Options.db_table - https://stackoverflow.com/questions/3519143/django-how-to-specify-a-database-for-a-model - And others, presumably
This commit is contained in:
		@@ -11,10 +11,10 @@ from django.contrib.auth import views as auth_views
 | 
			
		||||
from qr_code import urls as qr_code_urls
 | 
			
		||||
 | 
			
		||||
from company.urls import company_urls
 | 
			
		||||
from company.urls import supplier_part_urls
 | 
			
		||||
from company.urls import price_break_urls
 | 
			
		||||
 | 
			
		||||
from part.urls import part_urls
 | 
			
		||||
from part.urls import supplier_part_urls
 | 
			
		||||
from part.urls import price_break_urls
 | 
			
		||||
 | 
			
		||||
from stock.urls import stock_urls
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,22 @@ from django.contrib import admin
 | 
			
		||||
from import_export.admin import ImportExportModelAdmin
 | 
			
		||||
 | 
			
		||||
from .models import Company
 | 
			
		||||
from .models import SupplierPart
 | 
			
		||||
from .models import SupplierPriceBreak
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CompanyAdmin(ImportExportModelAdmin):
 | 
			
		||||
    list_display = ('name', 'website', 'contact')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartAdmin(ImportExportModelAdmin):
 | 
			
		||||
    list_display = ('part', 'supplier', 'SKU')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPriceBreakAdmin(ImportExportModelAdmin):
 | 
			
		||||
    list_display = ('part', 'quantity', 'cost')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
admin.site.register(Company, CompanyAdmin)
 | 
			
		||||
admin.site.register(SupplierPart, SupplierPartAdmin)
 | 
			
		||||
admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin)
 | 
			
		||||
@@ -12,7 +12,10 @@ from rest_framework import generics, permissions
 | 
			
		||||
from django.conf.urls import url
 | 
			
		||||
 | 
			
		||||
from .models import Company
 | 
			
		||||
from .models import SupplierPart, SupplierPriceBreak
 | 
			
		||||
 | 
			
		||||
from .serializers import CompanySerializer
 | 
			
		||||
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CompanyList(generics.ListCreateAPIView):
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ from __future__ import unicode_literals
 | 
			
		||||
from InvenTree.forms import HelperForm
 | 
			
		||||
 | 
			
		||||
from .models import Company
 | 
			
		||||
from .models import SupplierPart
 | 
			
		||||
from .models import SupplierPriceBreak
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditCompanyForm(HelperForm):
 | 
			
		||||
@@ -37,3 +39,37 @@ class CompanyImageForm(HelperForm):
 | 
			
		||||
        fields = [
 | 
			
		||||
            'image'
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditSupplierPartForm(HelperForm):
 | 
			
		||||
    """ Form for editing a SupplierPart object """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SupplierPart
 | 
			
		||||
        fields = [
 | 
			
		||||
            'part',
 | 
			
		||||
            'supplier',
 | 
			
		||||
            'SKU',
 | 
			
		||||
            'description',
 | 
			
		||||
            'manufacturer',
 | 
			
		||||
            'MPN',
 | 
			
		||||
            'URL',
 | 
			
		||||
            'note',
 | 
			
		||||
            'base_cost',
 | 
			
		||||
            'multiple',
 | 
			
		||||
            'minimum',
 | 
			
		||||
            'packaging',
 | 
			
		||||
            'lead_time'
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditPriceBreakForm(HelperForm):
 | 
			
		||||
    """ Form for creating / editing a supplier price break """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SupplierPriceBreak
 | 
			
		||||
        fields = [
 | 
			
		||||
            'part',
 | 
			
		||||
            'quantity',
 | 
			
		||||
            'cost'
 | 
			
		||||
        ]
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
# Generated by Django 2.2 on 2019-05-18 07:59
 | 
			
		||||
 | 
			
		||||
import django.core.validators
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('part', '0032_auto_20190518_1759'),
 | 
			
		||||
        ('company', '0006_auto_20190508_2332'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='SupplierPart',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('SKU', models.CharField(help_text='Supplier stock keeping unit', max_length=100)),
 | 
			
		||||
                ('manufacturer', models.CharField(blank=True, help_text='Manufacturer', max_length=100)),
 | 
			
		||||
                ('MPN', models.CharField(blank=True, help_text='Manufacturer part number', max_length=100)),
 | 
			
		||||
                ('URL', models.URLField(blank=True, help_text='URL for external supplier part link')),
 | 
			
		||||
                ('description', models.CharField(blank=True, help_text='Supplier part description', max_length=250)),
 | 
			
		||||
                ('note', models.CharField(blank=True, help_text='Notes', max_length=100)),
 | 
			
		||||
                ('base_cost', models.DecimalField(decimal_places=3, default=0, help_text='Minimum charge (e.g. stocking fee)', max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
 | 
			
		||||
                ('packaging', models.CharField(blank=True, help_text='Part packaging', max_length=50)),
 | 
			
		||||
                ('multiple', models.PositiveIntegerField(default=1, help_text='Order multiple', validators=[django.core.validators.MinValueValidator(1)])),
 | 
			
		||||
                ('minimum', models.PositiveIntegerField(default=1, help_text='Minimum order quantity (MOQ)', validators=[django.core.validators.MinValueValidator(1)])),
 | 
			
		||||
                ('lead_time', models.DurationField(blank=True, null=True)),
 | 
			
		||||
                ('part', models.ForeignKey(help_text='Select part', limit_choices_to={'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part')),
 | 
			
		||||
                ('supplier', models.ForeignKey(help_text='Select supplier', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='company.Company')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'db_table': 'part_supplierpart',
 | 
			
		||||
                'unique_together': {('part', 'supplier', 'SKU')},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='SupplierPriceBreak',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)])),
 | 
			
		||||
                ('cost', models.DecimalField(decimal_places=3, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
 | 
			
		||||
                ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pricebreaks', to='company.SupplierPart')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'db_table': 'part_supplierpricebreak',
 | 
			
		||||
                'unique_together': {('part', 'quantity')},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -7,6 +7,8 @@ from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from django.core.validators import MinValueValidator
 | 
			
		||||
 | 
			
		||||
from django.apps import apps
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
@@ -150,3 +152,174 @@ class Contact(models.Model):
 | 
			
		||||
 | 
			
		||||
    company = models.ForeignKey(Company, related_name='contacts',
 | 
			
		||||
                                on_delete=models.CASCADE)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPart(models.Model):
 | 
			
		||||
    """ Represents a unique part as provided by a Supplier
 | 
			
		||||
    Each SupplierPart is identified by a MPN (Manufacturer Part Number)
 | 
			
		||||
    Each SupplierPart is also linked to a Part object.
 | 
			
		||||
    A Part may be available from multiple suppliers
 | 
			
		||||
 | 
			
		||||
    Attributes:
 | 
			
		||||
        part: Link to the master Part
 | 
			
		||||
        supplier: Company that supplies this SupplierPart object
 | 
			
		||||
        SKU: Stock keeping unit (supplier part number)
 | 
			
		||||
        manufacturer: Manufacturer name
 | 
			
		||||
        MPN: Manufacture part number
 | 
			
		||||
        URL: Link to external website for this part
 | 
			
		||||
        description: Descriptive notes field
 | 
			
		||||
        note: Longer form note field
 | 
			
		||||
        base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee"
 | 
			
		||||
        multiple: Multiple that the part is provided in
 | 
			
		||||
        minimum: MOQ (minimum order quantity) required for purchase
 | 
			
		||||
        lead_time: Supplier lead time
 | 
			
		||||
        packaging: packaging that the part is supplied in, e.g. "Reel"
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse('supplier-part-detail', kwargs={'pk': self.id})
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = ('part', 'supplier', 'SKU')
 | 
			
		||||
 | 
			
		||||
        # This model was moved from the 'Part' app
 | 
			
		||||
        db_table = 'part_supplierpart'
 | 
			
		||||
 | 
			
		||||
    part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
 | 
			
		||||
                             related_name='supplier_parts',
 | 
			
		||||
                             limit_choices_to={'purchaseable': True},
 | 
			
		||||
                             help_text='Select part',
 | 
			
		||||
                             )
 | 
			
		||||
 | 
			
		||||
    supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
 | 
			
		||||
                                 related_name='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')
 | 
			
		||||
 | 
			
		||||
    MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
 | 
			
		||||
 | 
			
		||||
    URL = models.URLField(blank=True, help_text='URL for external supplier part link')
 | 
			
		||||
 | 
			
		||||
    description = models.CharField(max_length=250, blank=True, help_text='Supplier part description')
 | 
			
		||||
 | 
			
		||||
    note = models.CharField(max_length=100, blank=True, help_text='Notes')
 | 
			
		||||
 | 
			
		||||
    base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Minimum charge (e.g. stocking fee)')
 | 
			
		||||
 | 
			
		||||
    packaging = models.CharField(max_length=50, blank=True, help_text='Part packaging')
 | 
			
		||||
    
 | 
			
		||||
    multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Order multiple')
 | 
			
		||||
 | 
			
		||||
    minimum = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Minimum order quantity (MOQ)')
 | 
			
		||||
 | 
			
		||||
    lead_time = models.DurationField(blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def manufacturer_string(self):
 | 
			
		||||
 | 
			
		||||
        items = []
 | 
			
		||||
 | 
			
		||||
        if self.manufacturer:
 | 
			
		||||
            items.append(self.manufacturer)
 | 
			
		||||
        if self.MPN:
 | 
			
		||||
            items.append(self.MPN)
 | 
			
		||||
 | 
			
		||||
        return ' | '.join(items)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_price_breaks(self):
 | 
			
		||||
        return self.price_breaks.count() > 0
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def price_breaks(self):
 | 
			
		||||
        """ Return the associated price breaks in the correct order """
 | 
			
		||||
        return self.pricebreaks.order_by('quantity').all()
 | 
			
		||||
 | 
			
		||||
    def get_price(self, quantity, moq=True, multiples=True):
 | 
			
		||||
        """ Calculate the supplier price based on quantity price breaks.
 | 
			
		||||
 | 
			
		||||
        - Don't forget to add in flat-fee cost (base_cost field)
 | 
			
		||||
        - If MOQ (minimum order quantity) is required, bump quantity
 | 
			
		||||
        - If order multiples are to be observed, then we need to calculate based on that, too
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        price_breaks = self.price_breaks.all()
 | 
			
		||||
 | 
			
		||||
        # No price break information available?
 | 
			
		||||
        if len(price_breaks) == 0:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        # Minimum ordering requirement
 | 
			
		||||
        if moq and self.minimum > quantity:
 | 
			
		||||
            quantity = self.minimum
 | 
			
		||||
 | 
			
		||||
        # Order multiples
 | 
			
		||||
        if multiples:
 | 
			
		||||
            quantity = int(math.ceil(quantity / self.multipe) * self.multiple)
 | 
			
		||||
 | 
			
		||||
        pb_found = False
 | 
			
		||||
        pb_quantity = -1
 | 
			
		||||
        pb_cost = 0.0
 | 
			
		||||
 | 
			
		||||
        for pb in self.price_breaks.all():
 | 
			
		||||
            # Ignore this pricebreak (quantity is too high)
 | 
			
		||||
            if pb.quantity > quantity:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            pb_found = True
 | 
			
		||||
 | 
			
		||||
            # If this price-break quantity is the largest so far, use it!
 | 
			
		||||
            if pb.quantity > pb_quantity:
 | 
			
		||||
                pb_quantity = pb.quantity
 | 
			
		||||
                pb_cost = pb.cost
 | 
			
		||||
 | 
			
		||||
        if pb_found:
 | 
			
		||||
            cost = pb_cost * quantity
 | 
			
		||||
            return cost + self.base_cost
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        s = "{supplier} ({sku})".format(
 | 
			
		||||
            sku=self.SKU,
 | 
			
		||||
            supplier=self.supplier.name)
 | 
			
		||||
 | 
			
		||||
        if self.manufacturer_string:
 | 
			
		||||
            s = s + ' - ' + self.manufacturer_string
 | 
			
		||||
        
 | 
			
		||||
        return s
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPriceBreak(models.Model):
 | 
			
		||||
    """ Represents a quantity price break for a SupplierPart.
 | 
			
		||||
    - Suppliers can offer discounts at larger quantities
 | 
			
		||||
    - SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s)
 | 
			
		||||
 | 
			
		||||
    Attributes:
 | 
			
		||||
        part: Link to a SupplierPart object that this price break applies to
 | 
			
		||||
        quantity: Quantity required for price break
 | 
			
		||||
        cost: Cost at specified quantity
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks')
 | 
			
		||||
 | 
			
		||||
    quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)])
 | 
			
		||||
 | 
			
		||||
    cost = models.DecimalField(max_digits=10, decimal_places=3, validators=[MinValueValidator(0)])
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = ("part", "quantity")
 | 
			
		||||
 | 
			
		||||
        # This model was moved from the 'Part' app
 | 
			
		||||
        db_table = 'part_supplierpricebreak'
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "{mpn} - {cost} @ {quan}".format(
 | 
			
		||||
            mpn=self.part.MPN,
 | 
			
		||||
            cost=self.cost,
 | 
			
		||||
            quan=self.quantity)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,9 @@ JSON serializers for Company app
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from .models import Company
 | 
			
		||||
from .models import SupplierPart, SupplierPriceBreak
 | 
			
		||||
 | 
			
		||||
from part.serializers import PartBriefSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CompanyBriefSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -47,3 +50,43 @@ class CompanySerializer(serializers.ModelSerializer):
 | 
			
		||||
            'is_supplier',
 | 
			
		||||
            'part_count'
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """ Serializer for SupplierPart object """
 | 
			
		||||
 | 
			
		||||
    url = serializers.CharField(source='get_absolute_url', read_only=True)
 | 
			
		||||
 | 
			
		||||
    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_image_url', read_only=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SupplierPart
 | 
			
		||||
        fields = [
 | 
			
		||||
            'pk',
 | 
			
		||||
            'url',
 | 
			
		||||
            'part',
 | 
			
		||||
            'part_detail',
 | 
			
		||||
            'supplier',
 | 
			
		||||
            'supplier_name',
 | 
			
		||||
            'supplier_logo',
 | 
			
		||||
            'SKU',
 | 
			
		||||
            'manufacturer',
 | 
			
		||||
            'MPN',
 | 
			
		||||
            'URL',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPriceBreakSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """ Serializer for SupplierPriceBreak object """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SupplierPriceBreak
 | 
			
		||||
        fields = [
 | 
			
		||||
            'pk',
 | 
			
		||||
            'part',
 | 
			
		||||
            'quantity',
 | 
			
		||||
            'cost'
 | 
			
		||||
        ]
 | 
			
		||||
 
 | 
			
		||||
@@ -36,3 +36,23 @@ company_urls = [
 | 
			
		||||
    # Redirect any other patterns
 | 
			
		||||
    url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
price_break_urls = [
 | 
			
		||||
    url('^new/', views.PriceBreakCreate.as_view(), name='price-break-create'),
 | 
			
		||||
 | 
			
		||||
    url(r'^(?P<pk>\d+)/edit/', views.PriceBreakEdit.as_view(), name='price-break-edit'),
 | 
			
		||||
    url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
supplier_part_detail_urls = [
 | 
			
		||||
    url(r'edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
 | 
			
		||||
    url(r'delete/?', views.SupplierPartDelete.as_view(), name='supplier-part-delete'),
 | 
			
		||||
 | 
			
		||||
    url('^.*$', views.SupplierPartDetail.as_view(), name='supplier-part-detail'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
supplier_part_urls = [
 | 
			
		||||
    url(r'^new/?', views.SupplierPartCreate.as_view(), name='supplier-part-create'),
 | 
			
		||||
 | 
			
		||||
    url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
 | 
			
		||||
]
 | 
			
		||||
@@ -8,12 +8,18 @@ from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.views.generic import DetailView, ListView
 | 
			
		||||
 | 
			
		||||
from django.forms import HiddenInput
 | 
			
		||||
 | 
			
		||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
 | 
			
		||||
 | 
			
		||||
from .models import Company
 | 
			
		||||
from .models import SupplierPart
 | 
			
		||||
from .models import SupplierPriceBreak
 | 
			
		||||
 | 
			
		||||
from .forms import EditCompanyForm
 | 
			
		||||
from .forms import CompanyImageForm
 | 
			
		||||
from .forms import EditSupplierPartForm
 | 
			
		||||
from .forms import EditPriceBreakForm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CompanyIndex(ListView):
 | 
			
		||||
@@ -104,3 +110,142 @@ class CompanyDelete(AjaxDeleteView):
 | 
			
		||||
        return {
 | 
			
		||||
            'danger': 'Company was deleted',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartDetail(DetailView):
 | 
			
		||||
    """ Detail view for SupplierPart """
 | 
			
		||||
    model = SupplierPart
 | 
			
		||||
    template_name = 'company/partdetail.html'
 | 
			
		||||
    context_object_name = 'part'
 | 
			
		||||
    queryset = SupplierPart.objects.all()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartEdit(AjaxUpdateView):
 | 
			
		||||
    """ Update view for editing SupplierPart """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPart
 | 
			
		||||
    context_object_name = 'part'
 | 
			
		||||
    form_class = EditSupplierPartForm
 | 
			
		||||
    ajax_template_name = 'modal_form.html'
 | 
			
		||||
    ajax_form_title = 'Edit Supplier Part'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartCreate(AjaxCreateView):
 | 
			
		||||
    """ Create view for making new SupplierPart """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPart
 | 
			
		||||
    form_class = EditSupplierPartForm
 | 
			
		||||
    ajax_template_name = 'modal_form.html'
 | 
			
		||||
    ajax_form_title = 'Create new Supplier Part'
 | 
			
		||||
    context_object_name = 'part'
 | 
			
		||||
 | 
			
		||||
    def get_form(self):
 | 
			
		||||
        """ Create Form instance to create a new SupplierPart object.
 | 
			
		||||
        Hide some fields if they are not appropriate in context
 | 
			
		||||
        """
 | 
			
		||||
        form = super(AjaxCreateView, self).get_form()
 | 
			
		||||
        
 | 
			
		||||
        if form.initial.get('supplier', None):
 | 
			
		||||
            # Hide the supplier field
 | 
			
		||||
            form.fields['supplier'].widget = HiddenInput()
 | 
			
		||||
 | 
			
		||||
        if form.initial.get('part', None):
 | 
			
		||||
            # Hide the part field
 | 
			
		||||
            form.fields['part'].widget = HiddenInput()
 | 
			
		||||
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def get_initial(self):
 | 
			
		||||
        """ Provide initial data for new SupplierPart:
 | 
			
		||||
 | 
			
		||||
        - If 'supplier_id' provided, pre-fill supplier field
 | 
			
		||||
        - If 'part_id' provided, pre-fill part field
 | 
			
		||||
        """
 | 
			
		||||
        initials = super(SupplierPartCreate, self).get_initial().copy()
 | 
			
		||||
 | 
			
		||||
        supplier_id = self.get_param('supplier')
 | 
			
		||||
        part_id = self.get_param('part')
 | 
			
		||||
 | 
			
		||||
        if supplier_id:
 | 
			
		||||
            try:
 | 
			
		||||
                initials['supplier'] = Company.objects.get(pk=supplier_id)
 | 
			
		||||
            except Company.DoesNotExist:
 | 
			
		||||
                initials['supplier'] = None
 | 
			
		||||
        
 | 
			
		||||
        if part_id:
 | 
			
		||||
            try:
 | 
			
		||||
                initials['part'] = Part.objects.get(pk=part_id)
 | 
			
		||||
            except Part.DoesNotExist:
 | 
			
		||||
                initials['part'] = None
 | 
			
		||||
        
 | 
			
		||||
        return initials
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartDelete(AjaxDeleteView):
 | 
			
		||||
    """ Delete view for removing a SupplierPart """
 | 
			
		||||
    model = SupplierPart
 | 
			
		||||
    success_url = '/supplier/'
 | 
			
		||||
    ajax_template_name = 'company/partdelete.html'
 | 
			
		||||
    ajax_form_title = 'Delete Supplier Part'
 | 
			
		||||
    context_object_name = 'supplier_part'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PriceBreakCreate(AjaxCreateView):
 | 
			
		||||
    """ View for creating a supplier price break """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPriceBreak
 | 
			
		||||
    form_class = EditPriceBreakForm
 | 
			
		||||
    ajax_form_title = 'Add Price Break'
 | 
			
		||||
    ajax_template_name = 'modal_form.html'
 | 
			
		||||
 | 
			
		||||
    def get_data(self):
 | 
			
		||||
        return {
 | 
			
		||||
            'success': 'Added new price break'
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def get_part(self):
 | 
			
		||||
        try:
 | 
			
		||||
            return SupplierPart.objects.get(id=self.request.GET.get('part'))
 | 
			
		||||
        except SupplierPart.DoesNotExist:
 | 
			
		||||
            return SupplierPart.objects.get(id=self.request.POST.get('part'))
 | 
			
		||||
 | 
			
		||||
    def get_form(self):
 | 
			
		||||
 | 
			
		||||
        form = super(AjaxCreateView, self).get_form()
 | 
			
		||||
        form.fields['part'].widget = HiddenInput()
 | 
			
		||||
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def get_initial(self):
 | 
			
		||||
 | 
			
		||||
        initials = super(AjaxCreateView, self).get_initial()
 | 
			
		||||
 | 
			
		||||
        print("GETTING INITIAL DAtA")
 | 
			
		||||
 | 
			
		||||
        initials['part'] = self.get_part()
 | 
			
		||||
 | 
			
		||||
        return initials
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PriceBreakEdit(AjaxUpdateView):
 | 
			
		||||
    """ View for editing a supplier price break """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPriceBreak
 | 
			
		||||
    form_class = EditPriceBreakForm
 | 
			
		||||
    ajax_form_title = 'Edit Price Break'
 | 
			
		||||
    ajax_template_name = 'modal_form.html'
 | 
			
		||||
 | 
			
		||||
    def get_form(self):
 | 
			
		||||
 | 
			
		||||
        form = super(AjaxUpdateView, self).get_form()
 | 
			
		||||
        form.fields['part'].widget = HiddenInput()
 | 
			
		||||
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PriceBreakDelete(AjaxDeleteView):
 | 
			
		||||
    """ View for deleting a supplier price break """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPriceBreak
 | 
			
		||||
    ajax_form_title = "Delete Price Break"
 | 
			
		||||
    ajax_template_name = 'modal_delete_form.html'
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,5 @@ It includes models for:
 | 
			
		||||
 | 
			
		||||
- PartCategory
 | 
			
		||||
- Part
 | 
			
		||||
- SupplierPart
 | 
			
		||||
- BomItem
 | 
			
		||||
"""
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,6 @@ from import_export.admin import ImportExportModelAdmin
 | 
			
		||||
 | 
			
		||||
from .models import PartCategory, Part
 | 
			
		||||
from .models import PartAttachment, PartStar
 | 
			
		||||
from .models import SupplierPart
 | 
			
		||||
from .models import SupplierPriceBreak
 | 
			
		||||
from .models import BomItem
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -32,14 +30,6 @@ class BomItemAdmin(ImportExportModelAdmin):
 | 
			
		||||
    list_display = ('part', 'sub_part', 'quantity')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartAdmin(ImportExportModelAdmin):
 | 
			
		||||
    list_display = ('part', 'supplier', 'SKU')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPriceBreakAdmin(ImportExportModelAdmin):
 | 
			
		||||
    list_display = ('part', 'quantity', 'cost')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
class ParameterTemplateAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('name', 'units', 'format')
 | 
			
		||||
@@ -54,5 +44,4 @@ admin.site.register(PartCategory, PartCategoryAdmin)
 | 
			
		||||
admin.site.register(PartAttachment, PartAttachmentAdmin)
 | 
			
		||||
admin.site.register(PartStar, PartStarAdmin)
 | 
			
		||||
admin.site.register(BomItem, BomItemAdmin)
 | 
			
		||||
admin.site.register(SupplierPart, SupplierPartAdmin)
 | 
			
		||||
admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,11 +15,13 @@ from rest_framework import generics, permissions
 | 
			
		||||
from django.conf.urls import url, include
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from company.models import SupplierPart, SupplierPriceBreak
 | 
			
		||||
from company.serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
 | 
			
		||||
 | 
			
		||||
from .models import Part, PartCategory, BomItem, PartStar
 | 
			
		||||
from .models import SupplierPart, SupplierPriceBreak
 | 
			
		||||
 | 
			
		||||
from .serializers import PartSerializer, BomItemSerializer
 | 
			
		||||
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
 | 
			
		||||
from .serializers import CategorySerializer
 | 
			
		||||
from .serializers import PartStarSerializer
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@ from django import forms
 | 
			
		||||
 | 
			
		||||
from .models import Part, PartCategory, PartAttachment
 | 
			
		||||
from .models import BomItem
 | 
			
		||||
from .models import SupplierPart
 | 
			
		||||
from .models import SupplierPriceBreak
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartImageForm(HelperForm):
 | 
			
		||||
@@ -140,37 +138,3 @@ class EditBomItemForm(HelperForm):
 | 
			
		||||
 | 
			
		||||
        # Prevent editing of the part associated with this BomItem
 | 
			
		||||
        widgets = {'part': forms.HiddenInput()}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditSupplierPartForm(HelperForm):
 | 
			
		||||
    """ Form for editing a SupplierPart object """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SupplierPart
 | 
			
		||||
        fields = [
 | 
			
		||||
            'part',
 | 
			
		||||
            'supplier',
 | 
			
		||||
            'SKU',
 | 
			
		||||
            'description',
 | 
			
		||||
            'manufacturer',
 | 
			
		||||
            'MPN',
 | 
			
		||||
            'URL',
 | 
			
		||||
            'note',
 | 
			
		||||
            'base_cost',
 | 
			
		||||
            'multiple',
 | 
			
		||||
            'minimum',
 | 
			
		||||
            'packaging',
 | 
			
		||||
            'lead_time'
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditPriceBreakForm(HelperForm):
 | 
			
		||||
    """ Form for creating / editing a supplier price break """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SupplierPriceBreak
 | 
			
		||||
        fields = [
 | 
			
		||||
            'part',
 | 
			
		||||
            'quantity',
 | 
			
		||||
            'cost'
 | 
			
		||||
        ]
 | 
			
		||||
							
								
								
									
										34
									
								
								InvenTree/part/migrations/0032_auto_20190518_1759.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								InvenTree/part/migrations/0032_auto_20190518_1759.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
# Generated by Django 2.2 on 2019-05-18 07:59
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('stock', '0017_auto_20190518_1759'),
 | 
			
		||||
        ('part', '0031_auto_20190518_1650'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='supplierpricebreak',
 | 
			
		||||
            unique_together=None,
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='supplierpricebreak',
 | 
			
		||||
            name='part',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='default_supplier',
 | 
			
		||||
            field=models.ForeignKey(blank=True, help_text='Default supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='company.SupplierPart'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.DeleteModel(
 | 
			
		||||
            name='SupplierPart',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.DeleteModel(
 | 
			
		||||
            name='SupplierPriceBreak',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -32,7 +32,8 @@ import hashlib
 | 
			
		||||
from InvenTree import helpers
 | 
			
		||||
from InvenTree import validators
 | 
			
		||||
from InvenTree.models import InvenTreeTree
 | 
			
		||||
from company.models import Company
 | 
			
		||||
 | 
			
		||||
from company.models import SupplierPart
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartCategory(InvenTreeTree):
 | 
			
		||||
@@ -317,7 +318,7 @@ class Part(models.Model):
 | 
			
		||||
        # Default to None if there are multiple suppliers to choose from
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    default_supplier = models.ForeignKey('part.SupplierPart',
 | 
			
		||||
    default_supplier = models.ForeignKey(SupplierPart,
 | 
			
		||||
                                         on_delete=models.SET_NULL,
 | 
			
		||||
                                         blank=True, null=True,
 | 
			
		||||
                                         help_text='Default supplier part',
 | 
			
		||||
@@ -800,167 +801,3 @@ class BomItem(models.Model):
 | 
			
		||||
 | 
			
		||||
        return base_quantity + self.get_overage_quantity(base_quantity)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPart(models.Model):
 | 
			
		||||
    """ Represents a unique part as provided by a Supplier
 | 
			
		||||
    Each SupplierPart is identified by a MPN (Manufacturer Part Number)
 | 
			
		||||
    Each SupplierPart is also linked to a Part object.
 | 
			
		||||
    A Part may be available from multiple suppliers
 | 
			
		||||
 | 
			
		||||
    Attributes:
 | 
			
		||||
        part: Link to the master Part
 | 
			
		||||
        supplier: Company that supplies this SupplierPart object
 | 
			
		||||
        SKU: Stock keeping unit (supplier part number)
 | 
			
		||||
        manufacturer: Manufacturer name
 | 
			
		||||
        MPN: Manufacture part number
 | 
			
		||||
        URL: Link to external website for this part
 | 
			
		||||
        description: Descriptive notes field
 | 
			
		||||
        note: Longer form note field
 | 
			
		||||
        base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee"
 | 
			
		||||
        multiple: Multiple that the part is provided in
 | 
			
		||||
        minimum: MOQ (minimum order quantity) required for purchase
 | 
			
		||||
        lead_time: Supplier lead time
 | 
			
		||||
        packaging: packaging that the part is supplied in, e.g. "Reel"
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse('supplier-part-detail', kwargs={'pk': self.id})
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = ('part', 'supplier', 'SKU')
 | 
			
		||||
 | 
			
		||||
    part = models.ForeignKey(Part, on_delete=models.CASCADE,
 | 
			
		||||
                             related_name='supplier_parts',
 | 
			
		||||
                             limit_choices_to={'purchaseable': True},
 | 
			
		||||
                             help_text='Select part',
 | 
			
		||||
                             )
 | 
			
		||||
 | 
			
		||||
    supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
 | 
			
		||||
                                 related_name='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')
 | 
			
		||||
 | 
			
		||||
    MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
 | 
			
		||||
 | 
			
		||||
    URL = models.URLField(blank=True, help_text='URL for external supplier part link')
 | 
			
		||||
 | 
			
		||||
    description = models.CharField(max_length=250, blank=True, help_text='Supplier part description')
 | 
			
		||||
 | 
			
		||||
    note = models.CharField(max_length=100, blank=True, help_text='Notes')
 | 
			
		||||
 | 
			
		||||
    base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Minimum charge (e.g. stocking fee)')
 | 
			
		||||
 | 
			
		||||
    packaging = models.CharField(max_length=50, blank=True, help_text='Part packaging')
 | 
			
		||||
    
 | 
			
		||||
    multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Order multiple')
 | 
			
		||||
 | 
			
		||||
    minimum = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Minimum order quantity (MOQ)')
 | 
			
		||||
 | 
			
		||||
    lead_time = models.DurationField(blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def manufacturer_string(self):
 | 
			
		||||
 | 
			
		||||
        items = []
 | 
			
		||||
 | 
			
		||||
        if self.manufacturer:
 | 
			
		||||
            items.append(self.manufacturer)
 | 
			
		||||
        if self.MPN:
 | 
			
		||||
            items.append(self.MPN)
 | 
			
		||||
 | 
			
		||||
        return ' | '.join(items)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_price_breaks(self):
 | 
			
		||||
        return self.price_breaks.count() > 0
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def price_breaks(self):
 | 
			
		||||
        """ Return the associated price breaks in the correct order """
 | 
			
		||||
        return self.pricebreaks.order_by('quantity').all()
 | 
			
		||||
 | 
			
		||||
    def get_price(self, quantity, moq=True, multiples=True):
 | 
			
		||||
        """ Calculate the supplier price based on quantity price breaks.
 | 
			
		||||
 | 
			
		||||
        - Don't forget to add in flat-fee cost (base_cost field)
 | 
			
		||||
        - If MOQ (minimum order quantity) is required, bump quantity
 | 
			
		||||
        - If order multiples are to be observed, then we need to calculate based on that, too
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        price_breaks = self.price_breaks.all()
 | 
			
		||||
 | 
			
		||||
        # No price break information available?
 | 
			
		||||
        if len(price_breaks) == 0:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        # Minimum ordering requirement
 | 
			
		||||
        if moq and self.minimum > quantity:
 | 
			
		||||
            quantity = self.minimum
 | 
			
		||||
 | 
			
		||||
        # Order multiples
 | 
			
		||||
        if multiples:
 | 
			
		||||
            quantity = int(math.ceil(quantity / self.multipe) * self.multiple)
 | 
			
		||||
 | 
			
		||||
        pb_found = False
 | 
			
		||||
        pb_quantity = -1
 | 
			
		||||
        pb_cost = 0.0
 | 
			
		||||
 | 
			
		||||
        for pb in self.price_breaks.all():
 | 
			
		||||
            # Ignore this pricebreak (quantity is too high)
 | 
			
		||||
            if pb.quantity > quantity:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            pb_found = True
 | 
			
		||||
 | 
			
		||||
            # If this price-break quantity is the largest so far, use it!
 | 
			
		||||
            if pb.quantity > pb_quantity:
 | 
			
		||||
                pb_quantity = pb.quantity
 | 
			
		||||
                pb_cost = pb.cost
 | 
			
		||||
 | 
			
		||||
        if pb_found:
 | 
			
		||||
            cost = pb_cost * quantity
 | 
			
		||||
            return cost + self.base_cost
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        s = "{supplier} ({sku})".format(
 | 
			
		||||
            sku=self.SKU,
 | 
			
		||||
            supplier=self.supplier.name)
 | 
			
		||||
 | 
			
		||||
        if self.manufacturer_string:
 | 
			
		||||
            s = s + ' - ' + self.manufacturer_string
 | 
			
		||||
        
 | 
			
		||||
        return s
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPriceBreak(models.Model):
 | 
			
		||||
    """ Represents a quantity price break for a SupplierPart.
 | 
			
		||||
    - Suppliers can offer discounts at larger quantities
 | 
			
		||||
    - SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s)
 | 
			
		||||
 | 
			
		||||
    Attributes:
 | 
			
		||||
        part: Link to a SupplierPart object that this price break applies to
 | 
			
		||||
        quantity: Quantity required for price break
 | 
			
		||||
        cost: Cost at specified quantity
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks')
 | 
			
		||||
 | 
			
		||||
    quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)])
 | 
			
		||||
 | 
			
		||||
    cost = models.DecimalField(max_digits=10, decimal_places=3, validators=[MinValueValidator(0)])
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = ("part", "quantity")
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "{mpn} - {cost} @ {quan}".format(
 | 
			
		||||
            mpn=self.part.MPN,
 | 
			
		||||
            cost=self.cost,
 | 
			
		||||
            quan=self.quantity)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ JSON serializers for Part app
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from .models import Part, PartStar
 | 
			
		||||
from .models import SupplierPart, SupplierPriceBreak
 | 
			
		||||
 | 
			
		||||
from .models import PartCategory
 | 
			
		||||
from .models import BomItem
 | 
			
		||||
 | 
			
		||||
@@ -119,43 +119,3 @@ class BomItemSerializer(InvenTreeModelSerializer):
 | 
			
		||||
            'overage',
 | 
			
		||||
            'note',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """ Serializer for SupplierPart object """
 | 
			
		||||
 | 
			
		||||
    url = serializers.CharField(source='get_absolute_url', read_only=True)
 | 
			
		||||
 | 
			
		||||
    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_image_url', read_only=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SupplierPart
 | 
			
		||||
        fields = [
 | 
			
		||||
            'pk',
 | 
			
		||||
            'url',
 | 
			
		||||
            'part',
 | 
			
		||||
            'part_detail',
 | 
			
		||||
            'supplier',
 | 
			
		||||
            'supplier_name',
 | 
			
		||||
            'supplier_logo',
 | 
			
		||||
            'SKU',
 | 
			
		||||
            'manufacturer',
 | 
			
		||||
            'MPN',
 | 
			
		||||
            'URL',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPriceBreakSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """ Serializer for SupplierPriceBreak object """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SupplierPriceBreak
 | 
			
		||||
        fields = [
 | 
			
		||||
            'pk',
 | 
			
		||||
            'part',
 | 
			
		||||
            'quantity',
 | 
			
		||||
            'cost'
 | 
			
		||||
        ]
 | 
			
		||||
 
 | 
			
		||||
@@ -12,26 +12,6 @@ from django.conf.urls import url, include
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
price_break_urls = [
 | 
			
		||||
    url('^new/', views.PriceBreakCreate.as_view(), name='price-break-create'),
 | 
			
		||||
 | 
			
		||||
    url(r'^(?P<pk>\d+)/edit/', views.PriceBreakEdit.as_view(), name='price-break-edit'),
 | 
			
		||||
    url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
supplier_part_detail_urls = [
 | 
			
		||||
    url(r'edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
 | 
			
		||||
    url(r'delete/?', views.SupplierPartDelete.as_view(), name='supplier-part-delete'),
 | 
			
		||||
 | 
			
		||||
    url('^.*$', views.SupplierPartDetail.as_view(), name='supplier-part-detail'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
supplier_part_urls = [
 | 
			
		||||
    url(r'^new/?', views.SupplierPartCreate.as_view(), name='supplier-part-create'),
 | 
			
		||||
 | 
			
		||||
    url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
part_attachment_urls = [
 | 
			
		||||
    url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
 | 
			
		||||
    url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,6 @@ from django.forms import HiddenInput, CheckboxInput
 | 
			
		||||
from company.models import Company
 | 
			
		||||
from .models import PartCategory, Part, PartAttachment
 | 
			
		||||
from .models import BomItem
 | 
			
		||||
from .models import SupplierPart
 | 
			
		||||
from .models import SupplierPriceBreak
 | 
			
		||||
from .models import match_part_names
 | 
			
		||||
 | 
			
		||||
from . import forms as part_forms
 | 
			
		||||
@@ -732,142 +730,3 @@ class BomItemDelete(AjaxDeleteView):
 | 
			
		||||
    ajax_template_name = 'part/bom-delete.html'
 | 
			
		||||
    context_object_name = 'item'
 | 
			
		||||
    ajax_form_title = 'Confim BOM item deletion'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartDetail(DetailView):
 | 
			
		||||
    """ Detail view for SupplierPart """
 | 
			
		||||
    model = SupplierPart
 | 
			
		||||
    template_name = 'company/partdetail.html'
 | 
			
		||||
    context_object_name = 'part'
 | 
			
		||||
    queryset = SupplierPart.objects.all()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartEdit(AjaxUpdateView):
 | 
			
		||||
    """ Update view for editing SupplierPart """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPart
 | 
			
		||||
    context_object_name = 'part'
 | 
			
		||||
    form_class = part_forms.EditSupplierPartForm
 | 
			
		||||
    ajax_template_name = 'modal_form.html'
 | 
			
		||||
    ajax_form_title = 'Edit Supplier Part'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartCreate(AjaxCreateView):
 | 
			
		||||
    """ Create view for making new SupplierPart """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPart
 | 
			
		||||
    form_class = part_forms.EditSupplierPartForm
 | 
			
		||||
    ajax_template_name = 'modal_form.html'
 | 
			
		||||
    ajax_form_title = 'Create new Supplier Part'
 | 
			
		||||
    context_object_name = 'part'
 | 
			
		||||
 | 
			
		||||
    def get_form(self):
 | 
			
		||||
        """ Create Form instance to create a new SupplierPart object.
 | 
			
		||||
        Hide some fields if they are not appropriate in context
 | 
			
		||||
        """
 | 
			
		||||
        form = super(AjaxCreateView, self).get_form()
 | 
			
		||||
        
 | 
			
		||||
        if form.initial.get('supplier', None):
 | 
			
		||||
            # Hide the supplier field
 | 
			
		||||
            form.fields['supplier'].widget = HiddenInput()
 | 
			
		||||
 | 
			
		||||
        if form.initial.get('part', None):
 | 
			
		||||
            # Hide the part field
 | 
			
		||||
            form.fields['part'].widget = HiddenInput()
 | 
			
		||||
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def get_initial(self):
 | 
			
		||||
        """ Provide initial data for new SupplierPart:
 | 
			
		||||
 | 
			
		||||
        - If 'supplier_id' provided, pre-fill supplier field
 | 
			
		||||
        - If 'part_id' provided, pre-fill part field
 | 
			
		||||
        """
 | 
			
		||||
        initials = super(SupplierPartCreate, self).get_initial().copy()
 | 
			
		||||
 | 
			
		||||
        supplier_id = self.get_param('supplier')
 | 
			
		||||
        part_id = self.get_param('part')
 | 
			
		||||
 | 
			
		||||
        if supplier_id:
 | 
			
		||||
            try:
 | 
			
		||||
                initials['supplier'] = Company.objects.get(pk=supplier_id)
 | 
			
		||||
            except Company.DoesNotExist:
 | 
			
		||||
                initials['supplier'] = None
 | 
			
		||||
        
 | 
			
		||||
        if part_id:
 | 
			
		||||
            try:
 | 
			
		||||
                initials['part'] = Part.objects.get(pk=part_id)
 | 
			
		||||
            except Part.DoesNotExist:
 | 
			
		||||
                initials['part'] = None
 | 
			
		||||
        
 | 
			
		||||
        return initials
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplierPartDelete(AjaxDeleteView):
 | 
			
		||||
    """ Delete view for removing a SupplierPart """
 | 
			
		||||
    model = SupplierPart
 | 
			
		||||
    success_url = '/supplier/'
 | 
			
		||||
    ajax_template_name = 'company/partdelete.html'
 | 
			
		||||
    ajax_form_title = 'Delete Supplier Part'
 | 
			
		||||
    context_object_name = 'supplier_part'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PriceBreakCreate(AjaxCreateView):
 | 
			
		||||
    """ View for creating a supplier price break """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPriceBreak
 | 
			
		||||
    form_class = part_forms.EditPriceBreakForm
 | 
			
		||||
    ajax_form_title = 'Add Price Break'
 | 
			
		||||
    ajax_template_name = 'modal_form.html'
 | 
			
		||||
 | 
			
		||||
    def get_data(self):
 | 
			
		||||
        return {
 | 
			
		||||
            'success': 'Added new price break'
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def get_part(self):
 | 
			
		||||
        try:
 | 
			
		||||
            return SupplierPart.objects.get(id=self.request.GET.get('part'))
 | 
			
		||||
        except SupplierPart.DoesNotExist:
 | 
			
		||||
            return SupplierPart.objects.get(id=self.request.POST.get('part'))
 | 
			
		||||
 | 
			
		||||
    def get_form(self):
 | 
			
		||||
 | 
			
		||||
        form = super(AjaxCreateView, self).get_form()
 | 
			
		||||
        form.fields['part'].widget = HiddenInput()
 | 
			
		||||
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def get_initial(self):
 | 
			
		||||
 | 
			
		||||
        initials = super(AjaxCreateView, self).get_initial()
 | 
			
		||||
 | 
			
		||||
        print("GETTING INITIAL DAtA")
 | 
			
		||||
 | 
			
		||||
        initials['part'] = self.get_part()
 | 
			
		||||
 | 
			
		||||
        return initials
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PriceBreakEdit(AjaxUpdateView):
 | 
			
		||||
    """ View for editing a supplier price break """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPriceBreak
 | 
			
		||||
    form_class = part_forms.EditPriceBreakForm
 | 
			
		||||
    ajax_form_title = 'Edit Price Break'
 | 
			
		||||
    ajax_template_name = 'modal_form.html'
 | 
			
		||||
 | 
			
		||||
    def get_form(self):
 | 
			
		||||
 | 
			
		||||
        form = super(AjaxUpdateView, self).get_form()
 | 
			
		||||
        form.fields['part'].widget = HiddenInput()
 | 
			
		||||
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PriceBreakDelete(AjaxDeleteView):
 | 
			
		||||
    """ View for deleting a supplier price break """
 | 
			
		||||
 | 
			
		||||
    model = SupplierPriceBreak
 | 
			
		||||
    ajax_form_title = "Delete Price Break"
 | 
			
		||||
    ajax_template_name = 'modal_delete_form.html'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								InvenTree/stock/migrations/0017_auto_20190518_1759.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								InvenTree/stock/migrations/0017_auto_20190518_1759.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
# Generated by Django 2.2 on 2019-05-18 07:59
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('stock', '0016_auto_20190512_2119'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='stockitem',
 | 
			
		||||
            name='supplier_part',
 | 
			
		||||
            field=models.ForeignKey(blank=True, help_text='Select a matching supplier part for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, to='company.SupplierPart'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -22,6 +22,7 @@ from InvenTree import helpers
 | 
			
		||||
from InvenTree.models import InvenTreeTree
 | 
			
		||||
 | 
			
		||||
from part.models import Part
 | 
			
		||||
from company.models import SupplierPart
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockLocation(InvenTreeTree):
 | 
			
		||||
@@ -188,7 +189,7 @@ class StockItem(models.Model):
 | 
			
		||||
 | 
			
		||||
    part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='locations', help_text='Base part')
 | 
			
		||||
 | 
			
		||||
    supplier_part = models.ForeignKey('part.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
 | 
			
		||||
    supplier_part = models.ForeignKey(SupplierPart, blank=True, null=True, on_delete=models.SET_NULL,
 | 
			
		||||
                                      help_text='Select a matching supplier part for this stock item')
 | 
			
		||||
 | 
			
		||||
    location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								inventree_db.sqlite3.backup
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								inventree_db.sqlite3.backup
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user