mirror of
https://github.com/inventree/InvenTree.git
synced 2025-08-08 04:40:57 +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:
@@ -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'
|
||||
|
Reference in New Issue
Block a user