2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00

Moving 'supplier' to 'company'

This commit is contained in:
Oliver
2018-04-19 09:01:07 +10:00
parent 3bb434ae98
commit cef3c664f9
50 changed files with 95 additions and 132 deletions

View File

View File

@ -0,0 +1,21 @@
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import Company, SupplierPart
from .models import SupplierOrder
class CompanyAdmin(ImportExportModelAdmin):
list_display = ('name', 'website', 'contact')
class SupplierPartAdmin(ImportExportModelAdmin):
list_display = ('part', 'supplier', 'SKU')
class SupplierOrderAdmin(admin.ModelAdmin):
list_display = ('internal_ref', 'supplier', 'issued_date', 'delivery_date', 'status')
admin.site.register(Company, CompanyAdmin)
admin.site.register(SupplierPart, SupplierPartAdmin)
admin.site.register(SupplierOrder, SupplierOrderAdmin)

208
InvenTree/company/api.py Normal file
View File

@ -0,0 +1,208 @@
from django_filters.rest_framework import FilterSet, DjangoFilterBackend
from rest_framework import generics, permissions
from .models import Supplier, SupplierPart, SupplierPriceBreak
from .models import Manufacturer, Customer
from .serializers import SupplierSerializer
from .serializers import SupplierPartSerializer
from .serializers import SupplierPriceBreakSerializer
from .serializers import ManufacturerSerializer
from .serializers import CustomerSerializer
class ManufacturerDetail(generics.RetrieveUpdateDestroyAPIView):
"""
get:
Return a single Manufacturer
post:
Update a Manufacturer
delete:
Remove a Manufacturer
"""
queryset = Manufacturer.objects.all()
serializer_class = ManufacturerSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class ManufacturerList(generics.ListCreateAPIView):
"""
get:
Return a list of all Manufacturers
post:
Create a new Manufacturer
"""
queryset = Manufacturer.objects.all()
serializer_class = ManufacturerSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class CustomerDetail(generics.RetrieveUpdateDestroyAPIView):
"""
get:
Return a single Customer
post:
Update a Customer
delete:
Remove a Customer
"""
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class CustomerList(generics.ListCreateAPIView):
"""
get:
Return a list of all Cutstomers
post:
Create a new Customer
"""
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class SupplierDetail(generics.RetrieveUpdateDestroyAPIView):
"""
get:
Return a single Supplier
post:
Update a supplier
delete:
Remove a supplier
"""
queryset = Supplier.objects.all()
serializer_class = SupplierSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class SupplierList(generics.ListCreateAPIView):
"""
get:
Return a list of all Suppliers
post:
Create a new Supplier
"""
queryset = Supplier.objects.all()
serializer_class = SupplierSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
"""
get:
Return a single SupplierPart
post:
Update a SupplierPart
delete:
Remove a SupplierPart
"""
queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class SupplierPartFilter(FilterSet):
class Meta:
model = SupplierPart
fields = ['supplier', 'part', 'manufacturer']
class SupplierPartList(generics.ListCreateAPIView):
"""
get:
List all SupplierParts
(with optional query filters)
post:
Create a new SupplierPart
"""
queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
filter_backends = (DjangoFilterBackend,)
filter_class = SupplierPartFilter
class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView):
"""
get:
Return a single SupplierPriceBreak
post:
Update a SupplierPriceBreak
delete:
Remove a SupplierPriceBreak
"""
queryset = SupplierPriceBreak.objects.all()
serializer_class = SupplierPriceBreakSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class PriceBreakFilter(FilterSet):
class Meta:
model = SupplierPriceBreak
fields = ['part']
class SupplierPriceBreakList(generics.ListCreateAPIView):
"""
get:
Return a list of all SupplierPriceBreaks
(with optional query filters)
post:
Create a new SupplierPriceBreak
"""
queryset = SupplierPriceBreak.objects.all()
serializer_class = SupplierPriceBreakSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
filter_backends = (DjangoFilterBackend,)
filter_class = PriceBreakFilter

View File

@ -0,0 +1,7 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class CompanyConfig(AppConfig):
name = 'company'

View File

@ -0,0 +1,78 @@
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from .models import Company, SupplierPart
from .models import SupplierOrder
class EditSupplierOrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(EditSupplierOrderForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'id-edit-part-form'
self.helper.form_class = 'blueForms'
self.helper.form_method = 'post'
self.helper.add_input(Submit('submit', 'Submit'))
class Meta:
model = SupplierOrder
fields = [
'internal_ref',
'supplier',
'notes',
'issued_date',
]
class EditCompanyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(EditCompanyForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'id-edit-part-form'
self.helper.form_class = 'blueForms'
self.helper.form_method = 'post'
self.helper.add_input(Submit('submit', 'Submit'))
class Meta:
model = Company
fields = [
'name',
'description',
'website',
'address',
'phone',
'email',
'contact',
'notes'
]
class EditSupplierPartForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(EditSupplierPartForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'id-edit-part-form'
self.helper.form_class = 'blueForms'
self.helper.form_method = 'post'
self.helper.add_input(Submit('submit', 'Submit'))
class Meta:
model = SupplierPart
fields = [
'supplier',
'SKU',
'part',
'description',
'URL',
'manufacturer',
'MPN',
]

View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-12 05:02
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('part', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Customer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('website', models.URLField(blank=True)),
('address', models.CharField(blank=True, max_length=200)),
('phone', models.CharField(blank=True, max_length=50)),
('email', models.EmailField(blank=True, max_length=254)),
('contact', models.CharField(blank=True, max_length=100)),
('notes', models.CharField(blank=True, max_length=500)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Manufacturer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('website', models.URLField(blank=True)),
('address', models.CharField(blank=True, max_length=200)),
('phone', models.CharField(blank=True, max_length=50)),
('email', models.EmailField(blank=True, max_length=254)),
('contact', models.CharField(blank=True, max_length=100)),
('notes', models.CharField(blank=True, max_length=500)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Supplier',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('website', models.URLField(blank=True)),
('address', models.CharField(blank=True, max_length=200)),
('phone', models.CharField(blank=True, max_length=50)),
('email', models.EmailField(blank=True, max_length=254)),
('contact', models.CharField(blank=True, max_length=100)),
('notes', models.CharField(blank=True, max_length=500)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SupplierPart',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('SKU', models.CharField(max_length=100)),
('MPN', models.CharField(blank=True, max_length=100)),
('URL', models.URLField(blank=True)),
('description', models.CharField(blank=True, max_length=250)),
('single_price', models.DecimalField(decimal_places=3, default=0, max_digits=10)),
('base_cost', models.DecimalField(decimal_places=3, default=0, max_digits=10)),
('packaging', models.CharField(blank=True, max_length=50)),
('multiple', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)])),
('minimum', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)])),
('lead_time', models.DurationField(blank=True, null=True)),
('manufacturer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='supplier.Manufacturer')),
('part', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='part.Part')),
('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='supplier.Supplier')),
],
),
migrations.CreateModel(
name='SupplierPriceBreak',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(0)])),
('cost', models.DecimalField(decimal_places=3, max_digits=10)),
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='price_breaks', to='supplier.SupplierPart')),
],
),
migrations.AlterUniqueTogether(
name='supplierpricebreak',
unique_together=set([('part', 'quantity')]),
),
migrations.AlterUniqueTogether(
name='supplierpart',
unique_together=set([('part', 'supplier', 'SKU')]),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-12 06:22
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='supplierpart',
name='part',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'),
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-14 05:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('supplier', '0002_auto_20180412_0622'),
]
operations = [
migrations.AddField(
model_name='customer',
name='description',
field=models.CharField(blank=True, max_length=500),
),
migrations.AddField(
model_name='manufacturer',
name='description',
field=models.CharField(blank=True, max_length=500),
),
migrations.AddField(
model_name='supplier',
name='description',
field=models.CharField(blank=True, max_length=500),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-14 06:24
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0003_auto_20180414_0540'),
]
operations = [
migrations.AlterField(
model_name='supplierpart',
name='supplier',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='supplier.Supplier'),
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-15 02:55
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('supplier', '0004_auto_20180414_0624'),
]
operations = [
migrations.AlterField(
model_name='customer',
name='description',
field=models.CharField(max_length=500),
),
migrations.AlterField(
model_name='manufacturer',
name='description',
field=models.CharField(max_length=500),
),
migrations.AlterField(
model_name='supplier',
name='description',
field=models.CharField(max_length=500),
),
]

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-15 10:11
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0005_auto_20180415_0255'),
]
operations = [
migrations.AlterField(
model_name='supplierpart',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='supplier.Manufacturer'),
),
migrations.AlterField(
model_name='supplierpart',
name='part',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='supplier_parts', to='part.Part'),
),
]

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 12:53
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0006_auto_20180415_1011'),
]
operations = [
migrations.AlterField(
model_name='supplierpart',
name='MPN',
field=models.CharField(blank=True, help_text='Manufacturer part number', max_length=100),
),
migrations.AlterField(
model_name='supplierpart',
name='SKU',
field=models.CharField(help_text='Supplier stock keeping unit', max_length=100),
),
migrations.AlterField(
model_name='supplierpart',
name='manufacturer',
field=models.ForeignKey(blank=True, help_text='Manufacturer', null=True, on_delete=django.db.models.deletion.SET_NULL, to='supplier.Manufacturer'),
),
migrations.AlterField(
model_name='supplierpart',
name='part',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'),
),
]

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 13:37
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('supplier', '0007_auto_20180416_1253'),
]
operations = [
migrations.DeleteModel(
name='Customer',
),
]

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 14:11
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0008_delete_customer'),
]
operations = [
migrations.CreateModel(
name='SupplierOrder',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('internal_ref', models.CharField(max_length=25, unique=True)),
('created_date', models.DateField(auto_now_add=True)),
('issued_date', models.DateField(blank=True, help_text='Date the purchase order was issued')),
('notes', models.TextField(blank=True, help_text='Order notes')),
('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='supplier.Supplier')),
],
),
migrations.CreateModel(
name='SupplierOrderLineItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('line_number', models.PositiveIntegerField(default=1)),
('quantity', models.PositiveIntegerField(default=1)),
('notes', models.TextField(blank=True)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='supplier.SupplierOrder')),
('part', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='supplier.SupplierPart')),
],
),
migrations.AlterUniqueTogether(
name='supplierorderlineitem',
unique_together=set([('order', 'line_number'), ('order', 'part')]),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 15:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('supplier', '0008_delete_customer'),
]
operations = [
migrations.AlterField(
model_name='manufacturer',
name='notes',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='supplier',
name='notes',
field=models.TextField(blank=True),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 14:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('supplier', '0009_auto_20180417_1411'),
]
operations = [
migrations.AlterField(
model_name='supplierorder',
name='issued_date',
field=models.DateField(blank=True, help_text='Date the purchase order was issued', null=True),
),
]

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 14:36
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0010_auto_20180417_1420'),
]
operations = [
migrations.AlterField(
model_name='manufacturer',
name='address',
field=models.CharField(blank=True, help_text='Company address', max_length=200),
),
migrations.AlterField(
model_name='manufacturer',
name='name',
field=models.CharField(help_text='Company naem', max_length=100, unique=True),
),
migrations.AlterField(
model_name='manufacturer',
name='notes',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='manufacturer',
name='website',
field=models.URLField(blank=True, help_text='Company website URL'),
),
migrations.AlterField(
model_name='supplier',
name='address',
field=models.CharField(blank=True, help_text='Company address', max_length=200),
),
migrations.AlterField(
model_name='supplier',
name='name',
field=models.CharField(help_text='Company naem', max_length=100, unique=True),
),
migrations.AlterField(
model_name='supplier',
name='notes',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='supplier',
name='website',
field=models.URLField(blank=True, help_text='Company website URL'),
),
migrations.AlterField(
model_name='supplierorder',
name='supplier',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='supplier.Supplier'),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 14:47
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('supplier', '0011_auto_20180417_1436'),
]
operations = [
migrations.AddField(
model_name='supplierorder',
name='delivery_date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='supplierorder',
name='status',
field=models.PositiveIntegerField(choices=[(40, 'Cancelled'), (10, 'Pending'), (20, 'Placed'), (50, 'Lost'), (30, 'Received')], default=10),
),
]

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 15:17
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('supplier', '0009_auto_20180417_1516'),
('supplier', '0012_auto_20180417_1447'),
]
operations = [
]

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 15:20
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0022_auto_20180417_0819'),
('supplier', '0013_merge_20180417_1517'),
]
operations = [
migrations.RenameField(
model_name='supplierorderlineitem',
old_name='part',
new_name='supplier_part',
),
migrations.AddField(
model_name='supplierorderlineitem',
name='internal_part',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='part.Part'),
),
migrations.AlterUniqueTogether(
name='supplierorderlineitem',
unique_together=set([('order', 'line_number'), ('order', 'internal_part'), ('order', 'supplier_part')]),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 15:22
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('supplier', '0014_auto_20180417_1520'),
]
operations = [
migrations.AddField(
model_name='supplierorderlineitem',
name='received',
field=models.BooleanField(default=False),
),
]

View File

202
InvenTree/company/models.py Normal file
View File

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from django.db import models
from django.core.validators import MinValueValidator
from part.models import Part
class Company(models.Model):
""" Abstract model representing an external company
"""
name = models.CharField(max_length=100, unique=True,
help_text='Company naem')
description = models.CharField(max_length=500)
website = models.URLField(blank=True, help_text='Company website URL')
address = models.CharField(max_length=200,
blank=True, help_text='Company address')
phone = models.CharField(max_length=50,
blank=True)
email = models.EmailField(blank=True)
contact = models.CharField(max_length=100,
blank=True)
notes = models.TextField(blank=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return "/company/{id}/".format(id=self.id)
@property
def part_count(self):
return self.parts.count()
@property
def has_parts(self):
return self.part_count > 0
@property
def order_count(self):
return self.orders.count()
@property
def has_orders(self):
return self.order_count > 0
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
"""
def get_absolute_url(self):
return "/supplier/part/{id}/".format(id=self.id)
class Meta:
unique_together = ('part', 'supplier', 'SKU')
# Link to an actual part
# The part will have a field 'supplier_parts' which links to the supplier part options
part = models.ForeignKey(Part, on_delete=models.CASCADE,
related_name='supplier_parts')
supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
related_name='parts')
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)
description = models.CharField(max_length=250, blank=True)
# Default price for a single unit
single_price = models.DecimalField(max_digits=10, decimal_places=3, default=0)
# Base charge added to order independent of quantity e.g. "Reeling Fee"
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0)
# packaging that the part is supplied in, e.g. "Reel"
packaging = models.CharField(max_length=50, blank=True)
# multiple that the part is provided in
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)])
# Mimumum number required to order
minimum = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)])
# lead time for parts that cannot be delivered immediately
lead_time = models.DurationField(blank=True, null=True)
def __str__(self):
return "{sku} - {supplier}".format(
sku=self.SKU,
supplier=self.supplier.name)
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)
"""
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='price_breaks')
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)])
cost = models.DecimalField(max_digits=10, decimal_places=3)
class Meta:
unique_together = ("part", "quantity")
def __str__(self):
return "{mpn} - {cost}{currency} @ {quan}".format(
mpn=self.part.MPN,
cost=self.cost,
currency=self.currency if self.currency else '',
quan=self.quantity)
class SupplierOrder(models.Model):
"""
An order of parts from a supplier, made up of multiple line items
"""
def get_absolute_url(self):
return "/supplier/order/{id}/".format(id=self.id)
# Interal reference for this order
internal_ref = models.CharField(max_length=25, unique=True)
supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
related_name='orders')
created_date = models.DateField(auto_now_add=True, editable=False)
issued_date = models.DateField(blank=True, null=True, help_text="Date the purchase order was issued")
notes = models.TextField(blank=True, help_text="Order notes")
def __str__(self):
return "PO {ref} ({status})".format(ref=self.internal_ref,
status=self.get_status_display)
PENDING = 10 # Order is pending (not yet placed)
PLACED = 20 # Order has been placed
RECEIVED = 30 # Order has been received
CANCELLED = 40 # Order was cancelled
LOST = 50 # Order was lost
ORDER_STATUS_CODES = {PENDING: _("Pending"),
PLACED: _("Placed"),
CANCELLED: _("Cancelled"),
RECEIVED: _("Received"),
LOST: _("Lost")
}
status = models.PositiveIntegerField(default=PENDING,
choices=ORDER_STATUS_CODES.items())
delivery_date = models.DateField(blank=True, null=True)
class SupplierOrderLineItem(models.Model):
"""
A line item in a supplier order, corresponding to some quantity of part
"""
class Meta:
unique_together = [
('order', 'line_number'),
('order', 'supplier_part'),
('order', 'internal_part'),
]
order = models.ForeignKey(SupplierOrder, on_delete=models.CASCADE)
line_number = models.PositiveIntegerField(default=1)
internal_part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.SET_NULL)
supplier_part = models.ForeignKey(SupplierPart, null=True, blank=True, on_delete=models.SET_NULL)
quantity = models.PositiveIntegerField(default=1)
notes = models.TextField(blank=True)
received = models.BooleanField(default=False)

View File

@ -0,0 +1,55 @@
from rest_framework import serializers
from part.models import Part
from .models import Company, SupplierPart, SupplierPriceBreak
class CompanySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Company
fields = '__all__'
class SupplierPartSerializer(serializers.ModelSerializer):
price_breaks = serializers.HyperlinkedRelatedField(many=True,
read_only=True,
view_name='supplierpricebreak-detail')
part = serializers.HyperlinkedRelatedField(view_name='part-detail',
queryset=Part.objects.all())
supplier = serializers.HyperlinkedRelatedField(view_name='supplier-detail',
queryset=Supplier.objects.all())
manufacturer = serializers.HyperlinkedRelatedField(view_name='manufacturer-detail',
queryset=Manufacturer.objects.all())
class Meta:
model = SupplierPart
fields = ['url',
'part',
'supplier',
'SKU',
'manufacturer',
'MPN',
'URL',
'description',
'single_price',
'packaging',
'multiple',
'minimum',
'price_breaks',
'lead_time']
class SupplierPriceBreakSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SupplierPriceBreak
fields = ['url',
'part',
'quantity',
'cost']

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Create a new supplier
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "delete_obj.html" %}
{% block del_title %}
Are you sure you want to delete supplier '{{ supplier.name }}'?
{% endblock %}
{% block del_body %}
{% if supplier.part_count > 0 %}
<p>There are {{ supplier.part_count }} parts sourced from this supplier.<br>
If this supplier is deleted, these supplier part entries will also be deleted.</p>
<ul class='list-group'>
{% for part in supplier.parts.all %}
<li class='list-group-item'><b>{{ part.SKU }}</b><br><i>Part - {{ part.part.name }}</i></li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends "supplier/supplier_base.html" %}
{% block details %}
{% include "supplier/tabs.html" with tab='parts' %}
<h3>Supplier Parts</h3>
<table class="table table-striped">
<tr>
<th>SKU</th>
<th>Description</th>
<th>Parent Part</th>
<th>MPN</th>
<th>URL</th>
</tr>
{% for part in supplier.parts.all %}
<tr>
<td><a href="{% url 'supplier-part-detail' part.id %}">{{ part.SKU }}</a></td>
<td>{{ part.description }}</td>
<td>
{% if part.part %}
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.name }}</a>
{% endif %}
</td>
<td>
{% if part.manufacturer %}{{ part.manufacturer.name }}{% endif %}
{% if part.MPN %} | {{ part.MPN }}{% endif %}
</td>
<td>{{ part.URL }}</td>
{% endfor %}
</table>
<div class='container-fluid'>
<a href="{% url 'supplier-part-create' %}?supplier={{ supplier.id }}">
<button class="btn btn-success">New Supplier Part</button>
</a>
<a href="{% url 'supplier-edit' supplier.id %}">
<button class="btn btn-info">Edit supplier details</button>
</a>
<a href="{% url 'supplier-delete' supplier.id %}">
<button class="btn btn-danger">Delete supplier</button>
</a>
</div>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Edit details for supplier '{{ supplier.name }}'
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block content %}
<h3>Suppliers</h3>
<ul class='list-group'>
{% for supplier in suppliers %}
<li class='list-group-item'>
<b><a href="{% url 'supplier-detail' supplier.id %}">{{ supplier.name }}</a></b>
<br>
{{ supplier.description }}
{% if supplier.website %}
<a href="{{ supplier.website }}">- {{ supplier.website }}</a>
{% endif %}
<span class="badge">
{{ supplier.parts.all|length }}
</span>
</li>
{% endfor %}
</ul>
<div class='container-fluid'>
<a href="{% url 'supplier-create' %}">
<button class="btn btn-success">New Supplier</button>
</a>
</div>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Create a new supplier purchase order
{% endblock %}

View File

@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block content %}
<h3>Supplier Order Details</h3>
<table class='table table-striped'>
<tr>
<td>Reference</td>
<td>{{ order.internal_ref }}</td>
</tr>
<tr>
<td>Supplier</td>
<td>
{% if order.supplier %}
<a href="{% url 'supplier-detail-orders' order.supplier.id %}">{{ order.supplier.name }}</a>
{% endif %}
</td>
</tr>
<tr>
<td>Status</td>
<td>{% include "supplier/order_status.html" with order=order %}</td>
</tr>
<tr>
<td>Created Date</td>
<td>{{ order.created_date }}</td>
</tr>
<tr>
<td>Issued Date</td>
<td>{{ order.issued_date }}</td>
</tr>
<tr>
<td>Delivered Date</td>
<td>{{ order.delivery_date }}</td>
</tr>
</table>
{% if order.notes %}
<div class="panel panel-default">
<div class="panel-heading"><b>Notes</b></div>
<div class="panel-body">{{ order.notes }}</div>
</div>
{% endif %}
<h2>TODO</h2>
Here we list all the line ites which exist under this order...
{% endblock %}

View File

@ -0,0 +1,13 @@
{% if order.status == order.PENDING %}
<span class='label label-info'>
{% elif order.status == order.PLACED %}
<span class='label label-primary'>
{% elif order.status == order.RECEIVED %}
<span class='label label-success'>
{% elif order.status == order.CANCELLED %}
<span class='label label-warning'>
{% else %}
<span class='label label-danger'>
{% endif %}
{{ order.get_status_display }}
</span>

View File

@ -0,0 +1,31 @@
{% extends "supplier/supplier_base.html" %}
{% block details %}
{% include "supplier/tabs.html" with tab='order' %}
<h3>Supplier Orders</h3>
<table class="table table-striped">
<tr>
<th>Reference</th>
<th>Issued</th>
<th>Delivery</th>
<th>Status</th>
</tr>
{% for order in supplier.orders.all %}
<tr>
<td><a href="{% url 'supplier-order-detail' order.id %}">{{ order.internal_ref }}</a></td>
<td>{% if order.issued_date %}{{ order.issued_date }}{% endif %}</td>
<td>{% if order.delivery_date %}{{ order.delivery_date }}{% endif %}</td>
<td>{% include "supplier/order_status.html" with order=order %}</td>
</tr>
{% endfor %}
</table>
<div class='container-fluid'>
<a href="{% url 'supplier-order-create' %}?supplier={{ supplier.id }}">
<button class="btn btn-success">New Order</button>
</a>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Create a new supplier part
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends 'delete_obj.html' %}
{% block del_title %}
Are you sure you want to delete this supplier part?
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block content %}
<h3>Supplier part information</h3>
<table class="table">
<tr><td>SKU</td><td>{{ part.SKU }}</tr></tr>
<tr><td>Supplier</td><td><a href="{% url 'supplier-detail' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<tr>
<td>Parent Part</td>
<td>
{% if part.part %}
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.name }}</a>
{% endif %}
</td>
</tr>
{% if part.URL %}
<tr><td><URL></td><td><a href="{{ part.URL }}">{{ part.URL }}</a></td></tr>
{% endif %}
{% if part.description %}
<tr><td>Description</td><td>{{ part.description }}</td></tr>
{% endif %}
{% if part.manufacturer %}
<tr><td>Manufacturer</td><td>{% if part.manufacturer %}{{ part.manufacturer.name }}{% endif %}</td></tr>
<tr><td>MPN</td><td>{{ part.MPN }}</td></tr>
{% endif %}
</table>
<br>
<div class='container-fluid'>
<a href="{% url 'supplier-part-edit' part.id %}">
<button class="btn btn-info">Edit supplier part</button>
</a>
<a href="{% url 'supplier-part-delete' part.id %}">
<button class="btn btn-danger">Delete supplier part</button>
</a>
</div>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Edit supplier part '{{ part.SKU }}'
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-sm-6">
<h3>{{ supplier.name }}</h3>
<p>{{ supplier.description }}</p>
<p>{{ supplier.notes }}</p>
</div>
<div class="col-sm-6">
<table class="table">
{% if supplier.website %}
<tr>
<td>Website</td><td><a href="{{ supplier.website }}">{{ supplier.website }}</a></td>
</tr>
{% endif %}
{% if supplier.address %}
<tr>
<td>Address</td><td>{{ supplier.address }}</td>
</tr>
{% endif %}
{% if supplier.phone %}
<tr>
<td>Phone</td><td>{{ supplier.phone }}</td>
</tr>
{% endif %}
{% if supplier.email %}
<tr>
<td>Email</td><td>{{ supplier.email }}</td>
</tr>
{% endif %}
{% if supplier.contact %}
<tr>
<td>Contact</td><td>{{ supplier.contact }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
{% block details %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,6 @@
<ul class='nav nav-tabs'>
<li{% if tab == 'parts' %} class='active'{% endif %}>
<a href="{% url 'supplier-detail' supplier.id %}">Parts <span class='badge'>{{ supplier.part_count }}</span></a></li>
<li{% if tab == 'order' %} class='active'{% endif %}>
<a href="{% url 'supplier-detail-orders' supplier.id %}">Orders <span class="badge">{{ supplier.order_count }}</span></a></li>
</ul>

View File

@ -0,0 +1,3 @@
# from django.test import TestCase
# Create your tests here.

101
InvenTree/company/urls.py Normal file
View File

@ -0,0 +1,101 @@
from django.conf.urls import url, include
from django.views.generic.base import RedirectView
from . import views
"""
cust_urls = [
# Customer detail
url(r'^(?P<pk>[0-9]+)/?$', api.CustomerDetail.as_view(), name='customer-detail'),
# List customers
url(r'^\?.*/?$', api.CustomerList.as_view()),
url(r'^$', api.CustomerList.as_view())
]
manu_urls = [
# Manufacturer detail
url(r'^(?P<pk>[0-9]+)/?$', api.ManufacturerDetail.as_view(), name='manufacturer-detail'),
# List manufacturers
url(r'^\?.*/?$', api.ManufacturerList.as_view()),
url(r'^$', api.ManufacturerList.as_view())
]
supplier_api_part_urls = [
url(r'^(?P<pk>[0-9]+)/?$', api.SupplierPartDetail.as_view(), name='supplierpart-detail'),
url(r'^\?.*/?$', api.SupplierPartList.as_view()),
url(r'^$', api.SupplierPartList.as_view())
]
price_break_urls = [
url(r'^(?P<pk>[0-9]+)/?$', api.SupplierPriceBreakDetail.as_view(), name='supplierpricebreak-detail'),
url(r'^\?.*/?$', api.SupplierPriceBreakList.as_view()),
url(r'^$', api.SupplierPriceBreakList.as_view())
]
supplier_api_urls = [
# Display details of a supplier
url(r'^(?P<pk>[0-9]+)/$', api.SupplierDetail.as_view(), name='supplier-detail'),
# List suppliers
url(r'^\?.*/?$', api.SupplierList.as_view()),
url(r'^$', api.SupplierList.as_view())
]
"""
company_detail_urls = [
url(r'edit/?', views.CompanyEdit.as_view(), name='company-edit'),
url(r'delete/?', views.CompanyDelete.as_view(), name='company-delete'),
url(r'orders/?', views.CompanyDetail.as_view(template_name='supplier/orders.html'), name='company-detail-orders'),
url(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'),
]
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)),
]
"""
supplier_order_detail_urls = [
url('^.*$', views.SupplierOrderDetail.as_view(), name='supplier-order-detail'),
]
supplier_order_urls = [
url(r'^new/?', views.SupplierOrderCreate.as_view(), name='supplier-order-create'),
url(r'^(?P<pk>\d+)/', include(supplier_order_detail_urls)),
]
"""
company_urls = [
url(r'supplier_part/', include(supplier_part_urls)),
#url(r'order/', include(supplier_order_urls)),
#url(r'new/?', views.SupplierCreate.as_view(), name='supplier-create'),
url(r'^(?P<pk>\d+)/', include(company_detail_urls)),
url(r'', views.CompanyIndex.as_view(), name='company-index'),
# Redirect any other patterns
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'),
]

129
InvenTree/company/views.py Normal file
View File

@ -0,0 +1,129 @@
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView, DeleteView, CreateView
from part.models import Part
from .models import Company
from .models import SupplierPart
from .models import SupplierOrder
from .forms import EditCompanyForm
from .forms import EditSupplierPartForm
from .forms import EditSupplierOrderForm
class SupplierOrderDetail(DetailView):
context_object_name = 'order'
model = SupplierOrder
template_name = 'company/order_detail.html'
queryset = SupplierOrder.objects.all()
class SupplierOrderCreate(CreateView):
model = SupplierOrder
form_class = EditSupplierOrderForm
context_object_name = 'supplier'
template_name = 'company/order_create.html'
def get_initial(self):
initials = super(SupplierOrderCreate, self).get_initial().copy()
s_id = self.request.GET.get('supplier', None)
if s_id:
initials['supplier'] = get_object_or_404(Supplier, pk=s_id)
return initials
class CompanyIndex(ListView):
model = Company
template_name = 'company/index.html'
context_object_name = 'companies'
paginate_by = 50
def get_queryset(self):
return Supplier.objects.order_by('name')
class CompanyDetail(DetailView):
context_obect_name = 'company'
template_name = 'company/detail.html'
queryset = Company.objects.all()
model = Company
class CompanyEdit(UpdateView):
model = Company
form_class = EditCompanyForm
template_name = 'company/edit.html'
context_object_name = 'supplier'
class CompanyCreate(CreateView):
model = Company
form_class = EditCompanyForm
template_name = "company/create.html"
class CompanyDelete(DeleteView):
model = Company
success_url = '/company/'
template_name = 'company/delete.html'
def post(self, request, *args, **kwargs):
if 'confirm' in request.POST:
return super(CompanyDelete, self).post(request, *args, **kwargs)
else:
return HttpResponseRedirect(self.get_object().get_absolute_url())
class SupplierPartDetail(DetailView):
model = SupplierPart
template_name = 'company/partdetail.html'
context_object_name = 'part'
queryset = SupplierPart.objects.all()
class SupplierPartEdit(UpdateView):
model = SupplierPart
template_name = 'company/partedit.html'
context_object_name = 'part'
form_class = EditSupplierPartForm
class SupplierPartCreate(CreateView):
model = SupplierPart
form_class = EditSupplierPartForm
template_name = 'company/partcreate.html'
context_object_name = 'part'
def get_initial(self):
initials = super(SupplierPartCreate, self).get_initial().copy()
supplier_id = self.request.GET.get('supplier', None)
part_id = self.request.GET.get('part', None)
if supplier_id:
initials['supplier'] = get_object_or_404(Supplier, pk=supplier_id)
# TODO
# self.fields['supplier'].disabled = True
if part_id:
initials['part'] = get_object_or_404(Part, pk=part_id)
# TODO
# self.fields['part'].disabled = True
return initials
class SupplierPartDelete(DeleteView):
model = SupplierPart
success_url = '/supplier/'
template_name = 'company/partdelete.html'
def post(self, request, *args, **kwargs):
if 'confirm' in request.POST:
return super(SupplierPartDelete, self).post(request, *args, **kwargs)
else:
return HttpResponseRedirect(self.get_object().get_absolute_url())