mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
5043c354b1
commit
0cfb243eb3
@ -11,10 +11,10 @@ from django.contrib.auth import views as auth_views
|
|||||||
from qr_code import urls as qr_code_urls
|
from qr_code import urls as qr_code_urls
|
||||||
|
|
||||||
from company.urls import company_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 part_urls
|
||||||
from part.urls import supplier_part_urls
|
|
||||||
from part.urls import price_break_urls
|
|
||||||
|
|
||||||
from stock.urls import stock_urls
|
from stock.urls import stock_urls
|
||||||
|
|
||||||
|
@ -2,10 +2,22 @@ from django.contrib import admin
|
|||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart
|
||||||
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
|
|
||||||
class CompanyAdmin(ImportExportModelAdmin):
|
class CompanyAdmin(ImportExportModelAdmin):
|
||||||
list_display = ('name', 'website', 'contact')
|
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(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 django.conf.urls import url
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart, SupplierPriceBreak
|
||||||
|
|
||||||
from .serializers import CompanySerializer
|
from .serializers import CompanySerializer
|
||||||
|
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
|
||||||
|
|
||||||
|
|
||||||
class CompanyList(generics.ListCreateAPIView):
|
class CompanyList(generics.ListCreateAPIView):
|
||||||
|
@ -8,6 +8,8 @@ from __future__ import unicode_literals
|
|||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart
|
||||||
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
|
|
||||||
class EditCompanyForm(HelperForm):
|
class EditCompanyForm(HelperForm):
|
||||||
@ -37,3 +39,37 @@ class CompanyImageForm(HelperForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'image'
|
'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
|
import os
|
||||||
|
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -150,3 +152,174 @@ class Contact(models.Model):
|
|||||||
|
|
||||||
company = models.ForeignKey(Company, related_name='contacts',
|
company = models.ForeignKey(Company, related_name='contacts',
|
||||||
on_delete=models.CASCADE)
|
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 rest_framework import serializers
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart, SupplierPriceBreak
|
||||||
|
|
||||||
|
from part.serializers import PartBriefSerializer
|
||||||
|
|
||||||
|
|
||||||
class CompanyBriefSerializer(serializers.ModelSerializer):
|
class CompanyBriefSerializer(serializers.ModelSerializer):
|
||||||
@ -47,3 +50,43 @@ class CompanySerializer(serializers.ModelSerializer):
|
|||||||
'is_supplier',
|
'is_supplier',
|
||||||
'part_count'
|
'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
|
# Redirect any other patterns
|
||||||
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'),
|
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.views.generic import DetailView, ListView
|
||||||
|
|
||||||
|
from django.forms import HiddenInput
|
||||||
|
|
||||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart
|
||||||
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
from .forms import EditCompanyForm
|
from .forms import EditCompanyForm
|
||||||
from .forms import CompanyImageForm
|
from .forms import CompanyImageForm
|
||||||
|
from .forms import EditSupplierPartForm
|
||||||
|
from .forms import EditPriceBreakForm
|
||||||
|
|
||||||
|
|
||||||
class CompanyIndex(ListView):
|
class CompanyIndex(ListView):
|
||||||
@ -104,3 +110,142 @@ class CompanyDelete(AjaxDeleteView):
|
|||||||
return {
|
return {
|
||||||
'danger': 'Company was deleted',
|
'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
|
- PartCategory
|
||||||
- Part
|
- Part
|
||||||
- SupplierPart
|
|
||||||
- BomItem
|
- BomItem
|
||||||
"""
|
"""
|
||||||
|
@ -3,8 +3,6 @@ from import_export.admin import ImportExportModelAdmin
|
|||||||
|
|
||||||
from .models import PartCategory, Part
|
from .models import PartCategory, Part
|
||||||
from .models import PartAttachment, PartStar
|
from .models import PartAttachment, PartStar
|
||||||
from .models import SupplierPart
|
|
||||||
from .models import SupplierPriceBreak
|
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
|
|
||||||
|
|
||||||
@ -32,14 +30,6 @@ class BomItemAdmin(ImportExportModelAdmin):
|
|||||||
list_display = ('part', 'sub_part', 'quantity')
|
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):
|
class ParameterTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'units', 'format')
|
list_display = ('name', 'units', 'format')
|
||||||
@ -54,5 +44,4 @@ admin.site.register(PartCategory, PartCategoryAdmin)
|
|||||||
admin.site.register(PartAttachment, PartAttachmentAdmin)
|
admin.site.register(PartAttachment, PartAttachmentAdmin)
|
||||||
admin.site.register(PartStar, PartStarAdmin)
|
admin.site.register(PartStar, PartStarAdmin)
|
||||||
admin.site.register(BomItem, BomItemAdmin)
|
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.conf.urls import url, include
|
||||||
from django.urls import reverse
|
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 Part, PartCategory, BomItem, PartStar
|
||||||
from .models import SupplierPart, SupplierPriceBreak
|
|
||||||
|
|
||||||
from .serializers import PartSerializer, BomItemSerializer
|
from .serializers import PartSerializer, BomItemSerializer
|
||||||
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
|
|
||||||
from .serializers import CategorySerializer
|
from .serializers import CategorySerializer
|
||||||
from .serializers import PartStarSerializer
|
from .serializers import PartStarSerializer
|
||||||
|
|
||||||
|
@ -11,8 +11,6 @@ from django import forms
|
|||||||
|
|
||||||
from .models import Part, PartCategory, PartAttachment
|
from .models import Part, PartCategory, PartAttachment
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import SupplierPart
|
|
||||||
from .models import SupplierPriceBreak
|
|
||||||
|
|
||||||
|
|
||||||
class PartImageForm(HelperForm):
|
class PartImageForm(HelperForm):
|
||||||
@ -140,37 +138,3 @@ class EditBomItemForm(HelperForm):
|
|||||||
|
|
||||||
# Prevent editing of the part associated with this BomItem
|
# Prevent editing of the part associated with this BomItem
|
||||||
widgets = {'part': forms.HiddenInput()}
|
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 helpers
|
||||||
from InvenTree import validators
|
from InvenTree import validators
|
||||||
from InvenTree.models import InvenTreeTree
|
from InvenTree.models import InvenTreeTree
|
||||||
from company.models import Company
|
|
||||||
|
from company.models import SupplierPart
|
||||||
|
|
||||||
|
|
||||||
class PartCategory(InvenTreeTree):
|
class PartCategory(InvenTreeTree):
|
||||||
@ -317,7 +318,7 @@ class Part(models.Model):
|
|||||||
# Default to None if there are multiple suppliers to choose from
|
# Default to None if there are multiple suppliers to choose from
|
||||||
return None
|
return None
|
||||||
|
|
||||||
default_supplier = models.ForeignKey('part.SupplierPart',
|
default_supplier = models.ForeignKey(SupplierPart,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
help_text='Default supplier part',
|
help_text='Default supplier part',
|
||||||
@ -800,167 +801,3 @@ class BomItem(models.Model):
|
|||||||
|
|
||||||
return base_quantity + self.get_overage_quantity(base_quantity)
|
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 rest_framework import serializers
|
||||||
|
|
||||||
from .models import Part, PartStar
|
from .models import Part, PartStar
|
||||||
from .models import SupplierPart, SupplierPriceBreak
|
|
||||||
from .models import PartCategory
|
from .models import PartCategory
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
|
|
||||||
@ -119,43 +119,3 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
'overage',
|
'overage',
|
||||||
'note',
|
'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
|
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 = [
|
part_attachment_urls = [
|
||||||
url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
|
url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
|
||||||
url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
|
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 company.models import Company
|
||||||
from .models import PartCategory, Part, PartAttachment
|
from .models import PartCategory, Part, PartAttachment
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import SupplierPart
|
|
||||||
from .models import SupplierPriceBreak
|
|
||||||
from .models import match_part_names
|
from .models import match_part_names
|
||||||
|
|
||||||
from . import forms as part_forms
|
from . import forms as part_forms
|
||||||
@ -732,142 +730,3 @@ class BomItemDelete(AjaxDeleteView):
|
|||||||
ajax_template_name = 'part/bom-delete.html'
|
ajax_template_name = 'part/bom-delete.html'
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_form_title = 'Confim BOM item deletion'
|
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 InvenTree.models import InvenTreeTree
|
||||||
|
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
from company.models import SupplierPart
|
||||||
|
|
||||||
|
|
||||||
class StockLocation(InvenTreeTree):
|
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')
|
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')
|
help_text='Select a matching supplier part for this stock item')
|
||||||
|
|
||||||
location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,
|
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.
Loading…
Reference in New Issue
Block a user