diff --git a/.gitignore b/.gitignore
index 6229f979df..bfbaf7c285 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ var/
*.log
local_settings.py
*.sqlite3
+*.backup
# Sphinx files
docs/_build
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 2872096c08..1bffd90229 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -11,9 +11,10 @@ from django.contrib.auth import views as auth_views
from qr_code import urls as qr_code_urls
from company.urls import company_urls
+from company.urls import supplier_part_urls
+from company.urls import price_break_urls
from part.urls import part_urls
-from part.urls import supplier_part_urls
from stock.urls import stock_urls
@@ -50,6 +51,7 @@ apipatterns = [
urlpatterns = [
url(r'^part/', include(part_urls)),
url(r'^supplier-part/', include(supplier_part_urls)),
+ url(r'^price-break/', include(price_break_urls)),
url(r'^stock/', include(stock_urls)),
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 9d5e1a2dc4..f6fdbc4d46 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -223,6 +223,7 @@ class AjaxCreateView(AjaxMixin, CreateView):
super(CreateView, self).get(request, *args, **kwargs)
+ self.request = request
form = self.get_form()
return self.renderJsonResponse(request, form)
@@ -233,6 +234,7 @@ class AjaxCreateView(AjaxMixin, CreateView):
- If valid, save form
- Return status info (success / failure)
"""
+ self.request = request
form = self.get_form()
# Extra JSON data sent alongside form
diff --git a/InvenTree/build/migrations/0001_initial.py b/InvenTree/build/migrations/0001_initial.py
index 26e835b978..aa7356389a 100644
--- a/InvenTree/build/migrations/0001_initial.py
+++ b/InvenTree/build/migrations/0001_initial.py
@@ -1,7 +1,6 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-22 11:53
-from __future__ import unicode_literals
+# Generated by Django 2.2 on 2019-05-18 14:04
+from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
@@ -12,7 +11,9 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ('part', '0001_initial'),
+ ('part', '0002_auto_20190519_0004'),
+ ('stock', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
@@ -20,14 +21,29 @@ class Migration(migrations.Migration):
name='Build',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('batch', models.CharField(blank=True, help_text='Batch code for this build output', max_length=100, null=True)),
- ('status', models.PositiveIntegerField(choices=[(40, 'Complete'), (10, 'Pending'), (20, 'Holding'), (30, 'Cancelled')], default=10, validators=[django.core.validators.MinValueValidator(0)])),
- ('creation_date', models.DateField(auto_now=True)),
- ('completion_date', models.DateField(blank=True, null=True)),
('title', models.CharField(help_text='Brief description of the build', max_length=100)),
('quantity', models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)])),
- ('notes', models.TextField(blank=True)),
- ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part')),
+ ('status', models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status', validators=[django.core.validators.MinValueValidator(0)])),
+ ('batch', models.CharField(blank=True, help_text='Batch code for this build output', max_length=100, null=True)),
+ ('creation_date', models.DateField(auto_now=True)),
+ ('completion_date', models.DateField(blank=True, null=True)),
+ ('URL', models.URLField(blank=True, help_text='Link to external URL')),
+ ('notes', models.TextField(blank=True, help_text='Extra build notes')),
+ ('completed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_completed', to=settings.AUTH_USER_MODEL)),
+ ('part', models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part')),
+ ('take_from', models.ForeignKey(blank=True, help_text='Select location to take stock from for this build (leave blank to take from any stock location)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sourcing_builds', to='stock.StockLocation')),
],
),
+ migrations.CreateModel(
+ name='BuildItem',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('quantity', models.PositiveIntegerField(default=1, help_text='Stock quantity to allocate to build', validators=[django.core.validators.MinValueValidator(1)])),
+ ('build', models.ForeignKey(help_text='Build to allocate parts', on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build')),
+ ('stock_item', models.ForeignKey(help_text='Stock Item to allocate to build', on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem')),
+ ],
+ options={
+ 'unique_together': {('build', 'stock_item')},
+ },
+ ),
]
diff --git a/InvenTree/build/migrations/0002_auto_20190412_2030.py b/InvenTree/build/migrations/0002_auto_20190412_2030.py
deleted file mode 100644
index eca2bf5adb..0000000000
--- a/InvenTree/build/migrations/0002_auto_20190412_2030.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Generated by Django 2.2 on 2019-04-12 10:30
-
-import django.core.validators
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('build', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='build',
- name='part',
- field=models.ForeignKey(limit_choices_to={'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
- ),
- migrations.AlterField(
- model_name='build',
- name='status',
- field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Holding'), (30, 'Cancelled'), (40, 'Complete')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- ]
diff --git a/InvenTree/build/migrations/0003_builditemallocation.py b/InvenTree/build/migrations/0003_builditemallocation.py
deleted file mode 100644
index add13c7ac1..0000000000
--- a/InvenTree/build/migrations/0003_builditemallocation.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Generated by Django 2.2 on 2019-04-29 12:14
-
-import django.core.validators
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0009_auto_20190428_0841'),
- ('build', '0002_auto_20190412_2030'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='BuildItemAllocation',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('quantity', models.PositiveIntegerField(default=1, help_text='Stock quantity to allocate to build', validators=[django.core.validators.MinValueValidator(1)])),
- ('build', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build')),
- ('stock', models.ForeignKey(help_text='Stock Item to allocate to build', on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem')),
- ],
- ),
- ]
diff --git a/InvenTree/build/migrations/0004_build_url.py b/InvenTree/build/migrations/0004_build_url.py
deleted file mode 100644
index 187ac938d1..0000000000
--- a/InvenTree/build/migrations/0004_build_url.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-29 12:18
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('build', '0003_builditemallocation'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='build',
- name='URL',
- field=models.URLField(blank=True, help_text='Link to external URL'),
- ),
- ]
diff --git a/InvenTree/build/migrations/0005_auto_20190429_2229.py b/InvenTree/build/migrations/0005_auto_20190429_2229.py
deleted file mode 100644
index 73ebca7e1b..0000000000
--- a/InvenTree/build/migrations/0005_auto_20190429_2229.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-29 12:29
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0009_auto_20190428_0841'),
- ('build', '0004_build_url'),
- ]
-
- operations = [
- migrations.RenameModel(
- old_name='BuildItemAllocation',
- new_name='BuildItem',
- ),
- ]
diff --git a/InvenTree/build/migrations/0006_auto_20190429_2233.py b/InvenTree/build/migrations/0006_auto_20190429_2233.py
deleted file mode 100644
index 5d03e13b75..0000000000
--- a/InvenTree/build/migrations/0006_auto_20190429_2233.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-29 12:33
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('build', '0005_auto_20190429_2229'),
- ]
-
- operations = [
- migrations.RenameField(
- model_name='builditem',
- old_name='stock',
- new_name='stock_item',
- ),
- ]
diff --git a/InvenTree/build/migrations/0007_auto_20190429_2255.py b/InvenTree/build/migrations/0007_auto_20190429_2255.py
deleted file mode 100644
index 8a01606b84..0000000000
--- a/InvenTree/build/migrations/0007_auto_20190429_2255.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-29 12:55
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0009_auto_20190428_0841'),
- ('build', '0006_auto_20190429_2233'),
- ]
-
- operations = [
- migrations.AlterUniqueTogether(
- name='builditem',
- unique_together={('build', 'stock_item')},
- ),
- ]
diff --git a/InvenTree/build/migrations/0008_auto_20190501_2344.py b/InvenTree/build/migrations/0008_auto_20190501_2344.py
deleted file mode 100644
index febdd2d1b1..0000000000
--- a/InvenTree/build/migrations/0008_auto_20190501_2344.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Generated by Django 2.2 on 2019-05-01 13:44
-
-import django.core.validators
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('build', '0007_auto_20190429_2255'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='build',
- name='status',
- field=models.PositiveIntegerField(choices=[(10, 'Pending'), (30, 'Cancelled'), (40, 'Complete')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- migrations.AlterField(
- model_name='builditem',
- name='build',
- field=models.ForeignKey(help_text='Build to allocate parts', on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build'),
- ),
- ]
diff --git a/InvenTree/build/migrations/0009_build_completed_by.py b/InvenTree/build/migrations/0009_build_completed_by.py
deleted file mode 100644
index dc6c05a3f7..0000000000
--- a/InvenTree/build/migrations/0009_build_completed_by.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 2.2 on 2019-05-02 21:26
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('build', '0008_auto_20190501_2344'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='build',
- name='completed_by',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_completed', to=settings.AUTH_USER_MODEL),
- ),
- ]
diff --git a/InvenTree/build/migrations/0010_auto_20190505_2233.py b/InvenTree/build/migrations/0010_auto_20190505_2233.py
deleted file mode 100644
index 5be6c393c9..0000000000
--- a/InvenTree/build/migrations/0010_auto_20190505_2233.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-05-05 12:33
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('build', '0009_build_completed_by'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='build',
- name='part',
- field=models.ForeignKey(limit_choices_to={'active': True, 'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
- ),
- ]
diff --git a/InvenTree/build/migrations/0011_auto_20190508_0748.py b/InvenTree/build/migrations/0011_auto_20190508_0748.py
deleted file mode 100644
index e4c0ec5fa7..0000000000
--- a/InvenTree/build/migrations/0011_auto_20190508_0748.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-05-07 21:48
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('build', '0010_auto_20190505_2233'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='build',
- name='status',
- field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- ]
diff --git a/InvenTree/build/migrations/0012_auto_20190508_2332.py b/InvenTree/build/migrations/0012_auto_20190508_2332.py
deleted file mode 100644
index 099fce3fe9..0000000000
--- a/InvenTree/build/migrations/0012_auto_20190508_2332.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 2.2 on 2019-05-08 13:32
-
-import django.core.validators
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('build', '0011_auto_20190508_0748'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='build',
- name='notes',
- field=models.TextField(blank=True, help_text='Extra build notes'),
- ),
- migrations.AlterField(
- model_name='build',
- name='part',
- field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
- ),
- migrations.AlterField(
- model_name='build',
- name='status',
- field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status', validators=[django.core.validators.MinValueValidator(0)]),
- ),
- ]
diff --git a/InvenTree/build/migrations/0013_build_take_from.py b/InvenTree/build/migrations/0013_build_take_from.py
deleted file mode 100644
index 9945da2be1..0000000000
--- a/InvenTree/build/migrations/0013_build_take_from.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 2.2 on 2019-05-10 08:50
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0015_stockitem_delete_on_deplete'),
- ('build', '0012_auto_20190508_2332'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='build',
- name='take_from',
- field=models.ForeignKey(blank=True, help_text='Select location to take stock from for this build (leave blank to take from any stock location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sourcing_builds', to='stock.StockLocation'),
- ),
- ]
diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html
index 620db18df7..b2102ed74b 100644
--- a/InvenTree/build/templates/build/build_base.html
+++ b/InvenTree/build/templates/build/build_base.html
@@ -36,6 +36,19 @@ InvenTree | Build - {{ build }}
Quantity |
{{ build.quantity }} |
+
+ BOM Price |
+
+ {% if bom_price %}
+ {{ bom_price }}
+ {% if build.part.has_complete_bom_pricing == False %}
+ BOM pricing is incomplete
+ {% endif %}
+ {% else %}
+ No pricing information
+ {% endif %}
+ |
+
diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py
index 6f19c67754..14e22a3fe8 100644
--- a/InvenTree/build/views.py
+++ b/InvenTree/build/views.py
@@ -265,6 +265,16 @@ class BuildDetail(DetailView):
template_name = 'build/detail.html'
context_object_name = 'build'
+ def get_context_data(self, **kwargs):
+
+ ctx = super(DetailView, self).get_context_data(**kwargs)
+
+ build = self.get_object()
+
+ ctx['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
+
+ return ctx
+
class BuildAllocate(DetailView):
""" View for allocating parts to a Build """
diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py
index cbeff65cf8..607f773198 100644
--- a/InvenTree/company/admin.py
+++ b/InvenTree/company/admin.py
@@ -2,10 +2,22 @@ from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import Company
+from .models import SupplierPart
+from .models import SupplierPriceBreak
class CompanyAdmin(ImportExportModelAdmin):
list_display = ('name', 'website', 'contact')
+class SupplierPartAdmin(ImportExportModelAdmin):
+ list_display = ('part', 'supplier', 'SKU')
+
+
+class SupplierPriceBreakAdmin(ImportExportModelAdmin):
+ list_display = ('part', 'quantity', 'cost')
+
+
admin.site.register(Company, CompanyAdmin)
+admin.site.register(SupplierPart, SupplierPartAdmin)
+admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin)
diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py
index dfa3ebfb77..894974d3e9 100644
--- a/InvenTree/company/api.py
+++ b/InvenTree/company/api.py
@@ -9,10 +9,13 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from rest_framework import generics, permissions
-from django.conf.urls import url
+from django.conf.urls import url, include
from .models import Company
+from .models import SupplierPart, SupplierPriceBreak
+
from .serializers import CompanySerializer
+from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
class CompanyList(generics.ListCreateAPIView):
@@ -65,7 +68,85 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
]
+class SupplierPartList(generics.ListCreateAPIView):
+ """ API endpoint for list view of SupplierPart object
+
+ - GET: Return list of SupplierPart objects
+ - POST: Create a new SupplierPart object
+ """
+
+ queryset = SupplierPart.objects.all()
+ serializer_class = SupplierPartSerializer
+
+ permission_classes = [
+ permissions.IsAuthenticatedOrReadOnly,
+ ]
+
+ filter_backends = [
+ DjangoFilterBackend,
+ filters.SearchFilter,
+ filters.OrderingFilter,
+ ]
+
+ filter_fields = [
+ 'part',
+ 'supplier'
+ ]
+
+
+class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
+ """ API endpoint for detail view of SupplierPart object
+
+ - GET: Retrieve detail view
+ - PATCH: Update object
+ - DELETE: Delete objec
+ """
+
+ queryset = SupplierPart.objects.all()
+ serializer_class = SupplierPartSerializer
+ permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
+
+ read_only_fields = [
+ ]
+
+
+class SupplierPriceBreakList(generics.ListCreateAPIView):
+ """ API endpoint for list view of SupplierPriceBreak object
+
+ - GET: Retrieve list of SupplierPriceBreak objects
+ - POST: Create a new SupplierPriceBreak object
+ """
+
+ queryset = SupplierPriceBreak.objects.all()
+ serializer_class = SupplierPriceBreakSerializer
+
+ permission_classes = [
+ permissions.IsAuthenticatedOrReadOnly,
+ ]
+
+ filter_backends = [
+ DjangoFilterBackend,
+ ]
+
+ filter_fields = [
+ 'part',
+ ]
+
+
+supplier_part_api_urls = [
+
+ url(r'^(?P\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
+
+ # Catch anything else
+ url(r'^.*$', SupplierPartList.as_view(), name='api-part-supplier-list'),
+]
+
+
company_api_urls = [
+
+ url(r'^part/', include(supplier_part_api_urls)),
+
+ url(r'^price-break/?', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'),
url(r'^(?P\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),
diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py
index 679fe323ee..0ad63b9d63 100644
--- a/InvenTree/company/forms.py
+++ b/InvenTree/company/forms.py
@@ -8,6 +8,8 @@ from __future__ import unicode_literals
from InvenTree.forms import HelperForm
from .models import Company
+from .models import SupplierPart
+from .models import SupplierPriceBreak
class EditCompanyForm(HelperForm):
@@ -37,3 +39,37 @@ class CompanyImageForm(HelperForm):
fields = [
'image'
]
+
+
+class EditSupplierPartForm(HelperForm):
+ """ Form for editing a SupplierPart object """
+
+ class Meta:
+ model = SupplierPart
+ fields = [
+ 'part',
+ 'supplier',
+ 'SKU',
+ 'description',
+ 'manufacturer',
+ 'MPN',
+ 'URL',
+ 'note',
+ 'base_cost',
+ 'multiple',
+ 'minimum',
+ 'packaging',
+ 'lead_time'
+ ]
+
+
+class EditPriceBreakForm(HelperForm):
+ """ Form for creating / editing a supplier price break """
+
+ class Meta:
+ model = SupplierPriceBreak
+ fields = [
+ 'part',
+ 'quantity',
+ 'cost'
+ ]
diff --git a/InvenTree/company/migrations/0001_initial.py b/InvenTree/company/migrations/0001_initial.py
index 791011304c..b4da1df89e 100644
--- a/InvenTree/company/migrations/0001_initial.py
+++ b/InvenTree/company/migrations/0001_initial.py
@@ -1,9 +1,9 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-22 11:53
-from __future__ import unicode_literals
+# Generated by Django 2.2 on 2019-05-18 14:04
import company.models
+import django.core.validators
from django.db import migrations, models
+import django.db.models.deletion
class Migration(migrations.Migration):
@@ -18,15 +18,60 @@ class Migration(migrations.Migration):
name='Company',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(help_text='Company naem', max_length=100, unique=True)),
- ('description', models.CharField(max_length=500)),
+ ('name', models.CharField(help_text='Company name', max_length=100, unique=True)),
+ ('description', models.CharField(help_text='Description of the company', max_length=500)),
('website', models.URLField(blank=True, help_text='Company website URL')),
('address', models.CharField(blank=True, help_text='Company address', 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)),
+ ('phone', models.CharField(blank=True, help_text='Contact phone number', max_length=50)),
+ ('email', models.EmailField(blank=True, help_text='Contact email address', max_length=254)),
+ ('contact', models.CharField(blank=True, help_text='Point of contact', max_length=100)),
+ ('URL', models.URLField(blank=True, help_text='Link to external company information')),
('image', models.ImageField(blank=True, max_length=255, null=True, upload_to=company.models.rename_company_image)),
('notes', models.TextField(blank=True)),
+ ('is_customer', models.BooleanField(default=False, help_text='Do you sell items to this company?')),
+ ('is_supplier', models.BooleanField(default=True, help_text='Do you purchase items from this company?')),
],
),
+ migrations.CreateModel(
+ name='Contact',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ('phone', models.CharField(blank=True, max_length=100)),
+ ('email', models.EmailField(blank=True, max_length=254)),
+ ('role', models.CharField(blank=True, max_length=100)),
+ ],
+ ),
+ 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)),
+ ],
+ options={
+ 'db_table': 'part_supplierpart',
+ },
+ ),
+ 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=5, 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',
+ },
+ ),
]
diff --git a/InvenTree/company/migrations/0002_auto_20180422_1201.py b/InvenTree/company/migrations/0002_auto_20180422_1201.py
deleted file mode 100644
index 38578a58fe..0000000000
--- a/InvenTree/company/migrations/0002_auto_20180422_1201.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-22 12:01
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('company', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='company',
- name='is_customer',
- field=models.BooleanField(default=False),
- ),
- migrations.AddField(
- model_name='company',
- name='is_supplier',
- field=models.BooleanField(default=False),
- ),
- ]
diff --git a/InvenTree/part/migrations/0019_auto_20190508_2332.py b/InvenTree/company/migrations/0002_auto_20190519_0004.py
similarity index 51%
rename from InvenTree/part/migrations/0019_auto_20190508_2332.py
rename to InvenTree/company/migrations/0002_auto_20190519_0004.py
index b61bbee82b..c34c315d48 100644
--- a/InvenTree/part/migrations/0019_auto_20190508_2332.py
+++ b/InvenTree/company/migrations/0002_auto_20190519_0004.py
@@ -1,35 +1,40 @@
-# Generated by Django 2.2 on 2019-05-08 13:32
+# Generated by Django 2.2 on 2019-05-18 14:04
from django.db import migrations, models
import django.db.models.deletion
-import part.models
class Migration(migrations.Migration):
+ initial = True
+
dependencies = [
- ('part', '0018_auto_20190505_2231'),
+ ('company', '0001_initial'),
+ ('part', '0001_initial'),
]
operations = [
- migrations.AlterField(
- model_name='part',
- name='units',
- field=models.CharField(blank=True, default='pcs', help_text='Stock keeping units for this part', max_length=20),
- ),
- migrations.AlterField(
- model_name='partattachment',
- name='attachment',
- field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=part.models.attach_file),
- ),
- migrations.AlterField(
+ migrations.AddField(
model_name='supplierpart',
name='part',
field=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'),
),
- migrations.AlterField(
+ migrations.AddField(
model_name='supplierpart',
name='supplier',
field=models.ForeignKey(help_text='Select supplier', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='company.Company'),
),
+ migrations.AddField(
+ model_name='contact',
+ name='company',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='company.Company'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='supplierpricebreak',
+ unique_together={('part', 'quantity')},
+ ),
+ migrations.AlterUniqueTogether(
+ name='supplierpart',
+ unique_together={('part', 'supplier', 'SKU')},
+ ),
]
diff --git a/InvenTree/company/migrations/0003_auto_20180423_1117.py b/InvenTree/company/migrations/0003_auto_20180423_1117.py
deleted file mode 100644
index 638e3a1ab7..0000000000
--- a/InvenTree/company/migrations/0003_auto_20180423_1117.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-23 11:17
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('company', '0002_auto_20180422_1201'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='company',
- name='is_supplier',
- field=models.BooleanField(default=True),
- ),
- migrations.AlterField(
- model_name='company',
- name='name',
- field=models.CharField(help_text='Company name', max_length=100, unique=True),
- ),
- ]
diff --git a/InvenTree/company/migrations/0004_company_url.py b/InvenTree/company/migrations/0004_company_url.py
deleted file mode 100644
index e478757394..0000000000
--- a/InvenTree/company/migrations/0004_company_url.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-24 08:01
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('company', '0003_auto_20180423_1117'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='company',
- name='URL',
- field=models.URLField(blank=True, help_text='Link to external company information'),
- ),
- ]
diff --git a/InvenTree/company/migrations/0005_contact.py b/InvenTree/company/migrations/0005_contact.py
deleted file mode 100644
index 8a668a0590..0000000000
--- a/InvenTree/company/migrations/0005_contact.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-30 07:19
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('company', '0004_company_url'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Contact',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=100)),
- ('phone', models.CharField(blank=True, max_length=100)),
- ('email', models.EmailField(blank=True, max_length=254)),
- ('role', models.CharField(blank=True, max_length=100)),
- ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='company.Company')),
- ],
- ),
- ]
diff --git a/InvenTree/company/migrations/0006_auto_20190508_2332.py b/InvenTree/company/migrations/0006_auto_20190508_2332.py
deleted file mode 100644
index 232ccdd4f1..0000000000
--- a/InvenTree/company/migrations/0006_auto_20190508_2332.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Generated by Django 2.2 on 2019-05-08 13:32
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('company', '0005_contact'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='company',
- name='contact',
- field=models.CharField(blank=True, help_text='Point of contact', max_length=100),
- ),
- migrations.AlterField(
- model_name='company',
- name='description',
- field=models.CharField(help_text='Description of the company', max_length=500),
- ),
- migrations.AlterField(
- model_name='company',
- name='email',
- field=models.EmailField(blank=True, help_text='Contact email address', max_length=254),
- ),
- migrations.AlterField(
- model_name='company',
- name='is_customer',
- field=models.BooleanField(default=False, help_text='Do you sell items to this company?'),
- ),
- migrations.AlterField(
- model_name='company',
- name='is_supplier',
- field=models.BooleanField(default=True, help_text='Do you purchase items from this company?'),
- ),
- migrations.AlterField(
- model_name='company',
- name='phone',
- field=models.CharField(blank=True, help_text='Contact phone number', max_length=50),
- ),
- ]
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index 63fcf4add8..90a0df23a2 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -7,6 +7,10 @@ from __future__ import unicode_literals
import os
+import math
+
+from django.core.validators import MinValueValidator
+
from django.apps import apps
from django.db import models
from django.urls import reverse
@@ -150,3 +154,174 @@ class Contact(models.Model):
company = models.ForeignKey(Company, related_name='contacts',
on_delete=models.CASCADE)
+
+
+class SupplierPart(models.Model):
+ """ Represents a unique part as provided by a Supplier
+ Each SupplierPart is identified by a MPN (Manufacturer Part Number)
+ Each SupplierPart is also linked to a Part object.
+ A Part may be available from multiple suppliers
+
+ Attributes:
+ part: Link to the master Part
+ supplier: Company that supplies this SupplierPart object
+ SKU: Stock keeping unit (supplier part number)
+ manufacturer: Manufacturer name
+ MPN: Manufacture part number
+ URL: Link to external website for this part
+ description: Descriptive notes field
+ note: Longer form note field
+ base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee"
+ multiple: Multiple that the part is provided in
+ minimum: MOQ (minimum order quantity) required for purchase
+ lead_time: Supplier lead time
+ packaging: packaging that the part is supplied in, e.g. "Reel"
+ """
+
+ def get_absolute_url(self):
+ return reverse('supplier-part-detail', kwargs={'pk': self.id})
+
+ class Meta:
+ unique_together = ('part', 'supplier', 'SKU')
+
+ # This model was moved from the 'Part' app
+ db_table = 'part_supplierpart'
+
+ part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
+ related_name='supplier_parts',
+ limit_choices_to={'purchaseable': True},
+ help_text='Select part',
+ )
+
+ supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
+ related_name='parts',
+ limit_choices_to={'is_supplier': True},
+ help_text='Select supplier',
+ )
+
+ SKU = models.CharField(max_length=100, help_text='Supplier stock keeping unit')
+
+ manufacturer = models.CharField(max_length=100, blank=True, help_text='Manufacturer')
+
+ MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
+
+ URL = models.URLField(blank=True, help_text='URL for external supplier part link')
+
+ description = models.CharField(max_length=250, blank=True, help_text='Supplier part description')
+
+ note = models.CharField(max_length=100, blank=True, help_text='Notes')
+
+ base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Minimum charge (e.g. stocking fee)')
+
+ packaging = models.CharField(max_length=50, blank=True, help_text='Part packaging')
+
+ multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Order multiple')
+
+ minimum = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Minimum order quantity (MOQ)')
+
+ lead_time = models.DurationField(blank=True, null=True)
+
+ @property
+ def manufacturer_string(self):
+
+ items = []
+
+ if self.manufacturer:
+ items.append(self.manufacturer)
+ if self.MPN:
+ items.append(self.MPN)
+
+ return ' | '.join(items)
+
+ @property
+ def has_price_breaks(self):
+ return self.price_breaks.count() > 0
+
+ @property
+ def price_breaks(self):
+ """ Return the associated price breaks in the correct order """
+ return self.pricebreaks.order_by('quantity').all()
+
+ def get_price(self, quantity, moq=True, multiples=True):
+ """ Calculate the supplier price based on quantity price breaks.
+
+ - Don't forget to add in flat-fee cost (base_cost field)
+ - If MOQ (minimum order quantity) is required, bump quantity
+ - If order multiples are to be observed, then we need to calculate based on that, too
+ """
+
+ price_breaks = self.price_breaks.all()
+
+ # No price break information available?
+ if len(price_breaks) == 0:
+ return None
+
+ # Minimum ordering requirement
+ if moq and self.minimum > quantity:
+ quantity = self.minimum
+
+ # Order multiples
+ if multiples:
+ quantity = int(math.ceil(quantity / self.multiple) * 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=5, 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)
diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py
index 2967dbebd5..b09bccd234 100644
--- a/InvenTree/company/serializers.py
+++ b/InvenTree/company/serializers.py
@@ -5,6 +5,9 @@ JSON serializers for Company app
from rest_framework import serializers
from .models import Company
+from .models import SupplierPart, SupplierPriceBreak
+
+from part.serializers import PartBriefSerializer
class CompanyBriefSerializer(serializers.ModelSerializer):
@@ -47,3 +50,43 @@ class CompanySerializer(serializers.ModelSerializer):
'is_supplier',
'part_count'
]
+
+
+class SupplierPartSerializer(serializers.ModelSerializer):
+ """ Serializer for SupplierPart object """
+
+ url = serializers.CharField(source='get_absolute_url', read_only=True)
+
+ part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
+
+ supplier_name = serializers.CharField(source='supplier.name', read_only=True)
+ supplier_logo = serializers.CharField(source='supplier.get_image_url', read_only=True)
+
+ class Meta:
+ model = SupplierPart
+ fields = [
+ 'pk',
+ 'url',
+ 'part',
+ 'part_detail',
+ 'supplier',
+ 'supplier_name',
+ 'supplier_logo',
+ 'SKU',
+ 'manufacturer',
+ 'MPN',
+ 'URL',
+ ]
+
+
+class SupplierPriceBreakSerializer(serializers.ModelSerializer):
+ """ Serializer for SupplierPriceBreak object """
+
+ class Meta:
+ model = SupplierPriceBreak
+ fields = [
+ 'pk',
+ 'part',
+ 'quantity',
+ 'cost'
+ ]
diff --git a/InvenTree/company/templates/company/partdetail.html b/InvenTree/company/templates/company/partdetail.html
index b2fdf8aeb9..5d0eee89a9 100644
--- a/InvenTree/company/templates/company/partdetail.html
+++ b/InvenTree/company/templates/company/partdetail.html
@@ -10,7 +10,7 @@ InvenTree | {{ company.name }} - Parts
@@ -59,29 +59,46 @@ InvenTree | {{ company.name }} - Parts
- Pricing |
- Single Price | {{ part.single_price }} |
- {% if part.multiple > 1 %}
+
+ Pricing |
+
Order Multiple | {{ part.multiple }} |
- {% endif %}
{% if part.base_cost > 0 %}
Base Price (Flat Fee) | {{ part.base_cost }} |
{% endif %}
{% if part.minimum > 1 %}
Minimum Order Quantity | {{ part.minimum }} |
{% endif %}
- {% if part.price_breaks.all %}
- Price Breaks |
+
+ Price Breaks |
+
+
+
+
+ |
+
Quantity |
Price |
+ {% if part.price_breaks.all %}
{% for pb in part.price_breaks.all %}
{{ pb.quantity }} |
- {{ pb.cost }} |
+ {{ pb.cost }}
+
+
+
+
+ |
{% endfor %}
+ {% else %}
+
+
+ No price breaks have been added for this part
+ |
+
{% endif %}
@@ -89,9 +106,7 @@ InvenTree | {{ company.name }} - Parts
-
-
-
+
{% include 'modals.html' %}
@@ -116,4 +131,36 @@ InvenTree | {{ company.name }} - Parts
}
);
});
+
+ $('#new-price-break').click(function() {
+ launchModalForm("{% url 'price-break-create' %}",
+ {
+ reload: true,
+ data: {
+ part: {{ part.id }},
+ }
+ }
+ );
+ });
+
+ $('.pb-edit-button').click(function() {
+ var button = $(this);
+
+ launchModalForm(button.attr('url'),
+ {
+ reload: true,
+ }
+ );
+ });
+
+ $('.pb-delete-button').click(function() {
+ var button = $(this);
+
+ launchModalForm(button.attr('url'),
+ {
+ reload: true,
+ }
+ );
+ });
+
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py
index 0f8719fe01..7f617baafd 100644
--- a/InvenTree/company/urls.py
+++ b/InvenTree/company/urls.py
@@ -36,3 +36,23 @@ company_urls = [
# Redirect any other patterns
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'),
]
+
+price_break_urls = [
+ url('^new/', views.PriceBreakCreate.as_view(), name='price-break-create'),
+
+ url(r'^(?P\d+)/edit/', views.PriceBreakEdit.as_view(), name='price-break-edit'),
+ url(r'^(?P\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\d+)/', include(supplier_part_detail_urls)),
+]
diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py
index 0630556946..cce518676a 100644
--- a/InvenTree/company/views.py
+++ b/InvenTree/company/views.py
@@ -8,12 +8,20 @@ from __future__ import unicode_literals
from django.views.generic import DetailView, ListView
+from django.forms import HiddenInput
+
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from .models import Company
+from .models import SupplierPart
+from .models import SupplierPriceBreak
+
+from part.models import Part
from .forms import EditCompanyForm
from .forms import CompanyImageForm
+from .forms import EditSupplierPartForm
+from .forms import EditPriceBreakForm
class CompanyIndex(ListView):
@@ -104,3 +112,142 @@ class CompanyDelete(AjaxDeleteView):
return {
'danger': 'Company was deleted',
}
+
+
+class SupplierPartDetail(DetailView):
+ """ Detail view for SupplierPart """
+ model = SupplierPart
+ template_name = 'company/partdetail.html'
+ context_object_name = 'part'
+ queryset = SupplierPart.objects.all()
+
+
+class SupplierPartEdit(AjaxUpdateView):
+ """ Update view for editing SupplierPart """
+
+ model = SupplierPart
+ context_object_name = 'part'
+ form_class = EditSupplierPartForm
+ ajax_template_name = 'modal_form.html'
+ ajax_form_title = 'Edit Supplier Part'
+
+
+class SupplierPartCreate(AjaxCreateView):
+ """ Create view for making new SupplierPart """
+
+ model = SupplierPart
+ form_class = EditSupplierPartForm
+ ajax_template_name = 'modal_form.html'
+ ajax_form_title = 'Create new Supplier Part'
+ context_object_name = 'part'
+
+ def get_form(self):
+ """ Create Form instance to create a new SupplierPart object.
+ Hide some fields if they are not appropriate in context
+ """
+ form = super(AjaxCreateView, self).get_form()
+
+ if form.initial.get('supplier', None):
+ # Hide the supplier field
+ form.fields['supplier'].widget = HiddenInput()
+
+ if form.initial.get('part', None):
+ # Hide the part field
+ form.fields['part'].widget = HiddenInput()
+
+ return form
+
+ def get_initial(self):
+ """ Provide initial data for new SupplierPart:
+
+ - If 'supplier_id' provided, pre-fill supplier field
+ - If 'part_id' provided, pre-fill part field
+ """
+ initials = super(SupplierPartCreate, self).get_initial().copy()
+
+ supplier_id = self.get_param('supplier')
+ part_id = self.get_param('part')
+
+ if supplier_id:
+ try:
+ initials['supplier'] = Company.objects.get(pk=supplier_id)
+ except Company.DoesNotExist:
+ initials['supplier'] = None
+
+ if part_id:
+ try:
+ initials['part'] = Part.objects.get(pk=part_id)
+ except Part.DoesNotExist:
+ initials['part'] = None
+
+ return initials
+
+
+class SupplierPartDelete(AjaxDeleteView):
+ """ Delete view for removing a SupplierPart """
+ model = SupplierPart
+ success_url = '/supplier/'
+ ajax_template_name = 'company/partdelete.html'
+ ajax_form_title = 'Delete Supplier Part'
+ context_object_name = 'supplier_part'
+
+
+class PriceBreakCreate(AjaxCreateView):
+ """ View for creating a supplier price break """
+
+ model = SupplierPriceBreak
+ form_class = EditPriceBreakForm
+ ajax_form_title = 'Add Price Break'
+ ajax_template_name = 'modal_form.html'
+
+ def get_data(self):
+ return {
+ 'success': 'Added new price break'
+ }
+
+ def get_part(self):
+ try:
+ return SupplierPart.objects.get(id=self.request.GET.get('part'))
+ except SupplierPart.DoesNotExist:
+ return SupplierPart.objects.get(id=self.request.POST.get('part'))
+
+ def get_form(self):
+
+ form = super(AjaxCreateView, self).get_form()
+ form.fields['part'].widget = HiddenInput()
+
+ return form
+
+ def get_initial(self):
+
+ initials = super(AjaxCreateView, self).get_initial()
+
+ print("GETTING INITIAL DAtA")
+
+ initials['part'] = self.get_part()
+
+ return initials
+
+
+class PriceBreakEdit(AjaxUpdateView):
+ """ View for editing a supplier price break """
+
+ model = SupplierPriceBreak
+ form_class = EditPriceBreakForm
+ ajax_form_title = 'Edit Price Break'
+ ajax_template_name = 'modal_form.html'
+
+ def get_form(self):
+
+ form = super(AjaxUpdateView, self).get_form()
+ form.fields['part'].widget = HiddenInput()
+
+ return form
+
+
+class PriceBreakDelete(AjaxDeleteView):
+ """ View for deleting a supplier price break """
+
+ model = SupplierPriceBreak
+ ajax_form_title = "Delete Price Break"
+ ajax_template_name = 'modal_delete_form.html'
diff --git a/InvenTree/part/__init__.py b/InvenTree/part/__init__.py
index da73be7753..e5912481ff 100644
--- a/InvenTree/part/__init__.py
+++ b/InvenTree/part/__init__.py
@@ -5,6 +5,5 @@ It includes models for:
- PartCategory
- Part
-- SupplierPart
- BomItem
"""
diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py
index 18e20cb9ee..136d486b0b 100644
--- a/InvenTree/part/admin.py
+++ b/InvenTree/part/admin.py
@@ -3,7 +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 BomItem
@@ -31,10 +30,6 @@ class BomItemAdmin(ImportExportModelAdmin):
list_display = ('part', 'sub_part', 'quantity')
-class SupplierPartAdmin(ImportExportModelAdmin):
- list_display = ('part', 'supplier', 'SKU')
-
-
"""
class ParameterTemplateAdmin(admin.ModelAdmin):
list_display = ('name', 'units', 'format')
@@ -49,4 +44,3 @@ 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)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 0973138b21..aa065e9720 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -16,10 +16,8 @@ from django.conf.urls import url, include
from django.urls import reverse
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
@@ -233,71 +231,6 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
]
-class SupplierPartList(generics.ListCreateAPIView):
- """ API endpoint for list view of SupplierPart object
-
- - GET: Return list of SupplierPart objects
- - POST: Create a new SupplierPart object
- """
-
- queryset = SupplierPart.objects.all()
- serializer_class = SupplierPartSerializer
-
- permission_classes = [
- permissions.IsAuthenticatedOrReadOnly,
- ]
-
- filter_backends = [
- DjangoFilterBackend,
- filters.SearchFilter,
- filters.OrderingFilter,
- ]
-
- filter_fields = [
- 'part',
- 'supplier'
- ]
-
-
-class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
- """ API endpoint for detail view of SupplierPart object
-
- - GET: Retrieve detail view
- - PATCH: Update object
- - DELETE: Delete objec
- """
-
- queryset = SupplierPart.objects.all()
- serializer_class = SupplierPartSerializer
- permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
-
- read_only_fields = [
- ]
-
-
-class SupplierPriceBreakList(generics.ListCreateAPIView):
- """ API endpoint for list view of SupplierPriceBreak object
-
- - GET: Retrieve list of SupplierPriceBreak objects
- - POST: Create a new SupplierPriceBreak object
- """
-
- queryset = SupplierPriceBreak.objects.all()
- serializer_class = SupplierPriceBreakSerializer
-
- permission_classes = [
- permissions.IsAuthenticatedOrReadOnly,
- ]
-
- filter_backends = [
- DjangoFilterBackend,
- ]
-
- filter_fields = [
- 'part',
- ]
-
-
cat_api_urls = [
url(r'^(?P\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
@@ -305,13 +238,6 @@ cat_api_urls = [
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
]
-supplier_part_api_urls = [
-
- url(r'^(?P\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
-
- # Catch anything else
- url(r'^.*$', SupplierPartList.as_view(), name='api-part-supplier-list'),
-]
part_star_api_urls = [
url(r'^(?P\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'),
@@ -320,21 +246,19 @@ part_star_api_urls = [
url(r'^.*$', PartStarList.as_view(), name='api-part-star-list'),
]
+
part_api_urls = [
url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),
url(r'^category/', include(cat_api_urls)),
- url(r'^supplier/', include(supplier_part_api_urls)),
-
url(r'^star/', include(part_star_api_urls)),
- url(r'^price-break/?', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'),
-
url(r'^(?P\d+)/', PartDetail.as_view(), name='api-part-detail'),
url(r'^.*$', PartList.as_view(), name='api-part-list'),
]
+
bom_api_urls = [
# BOM Item Detail
url('^(?P\d+)/', BomDetail.as_view(), name='api-bom-detail'),
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index d4e70ee47a..e069df6ed6 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -11,7 +11,6 @@ from django import forms
from .models import Part, PartCategory, PartAttachment
from .models import BomItem
-from .models import SupplierPart
class PartImageForm(HelperForm):
@@ -141,24 +140,17 @@ class EditBomItemForm(HelperForm):
widgets = {'part': forms.HiddenInput()}
-class EditSupplierPartForm(HelperForm):
- """ Form for editing a SupplierPart object """
+class PartPriceForm(forms.Form):
+ """ Simple form for viewing part pricing information """
+
+ quantity = forms.IntegerField(
+ required=True,
+ initial=1,
+ help_text='Input quantity for price calculation'
+ )
class Meta:
- model = SupplierPart
+ model = Part
fields = [
- 'part',
- 'supplier',
- 'SKU',
- 'description',
- 'manufacturer',
- 'MPN',
- 'URL',
- 'note',
- 'single_price',
- 'base_cost',
- 'multiple',
- 'minimum',
- 'packaging',
- 'lead_time'
+ 'quantity'
]
diff --git a/InvenTree/part/migrations/0001_initial.py b/InvenTree/part/migrations/0001_initial.py
index 361436047e..c0413a909a 100644
--- a/InvenTree/part/migrations/0001_initial.py
+++ b/InvenTree/part/migrations/0001_initial.py
@@ -1,7 +1,7 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-22 11:53
-from __future__ import unicode_literals
+# Generated by Django 2.2 on 2019-05-18 14:04
+import InvenTree.validators
+from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
@@ -13,7 +13,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ('company', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
@@ -21,7 +21,9 @@ class Migration(migrations.Migration):
name='BomItem',
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(0)])),
+ ('quantity', models.PositiveIntegerField(default=1, help_text='BOM quantity for this BOM item', validators=[django.core.validators.MinValueValidator(0)])),
+ ('overage', models.CharField(blank=True, help_text='Estimated build wastage quantity (absolute or percentage)', max_length=24, validators=[InvenTree.validators.validate_overage])),
+ ('note', models.CharField(blank=True, help_text='BOM item notes', max_length=100)),
],
options={
'verbose_name': 'BOM Item',
@@ -31,18 +33,24 @@ class Migration(migrations.Migration):
name='Part',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(help_text='Part name (must be unique)', max_length=100, unique=True)),
+ ('name', models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name])),
+ ('variant', models.CharField(blank=True, help_text='Part variant or revision code', max_length=32)),
('description', models.CharField(help_text='Part description', max_length=250)),
+ ('keywords', models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250)),
('IPN', models.CharField(blank=True, help_text='Internal Part Number', max_length=100)),
('URL', models.URLField(blank=True, help_text='Link to extenal URL')),
('image', models.ImageField(blank=True, max_length=255, null=True, upload_to=part.models.rename_part_image)),
('minimum_stock', models.PositiveIntegerField(default=0, help_text='Minimum allowed stock level', validators=[django.core.validators.MinValueValidator(0)])),
- ('units', models.CharField(blank=True, default='pcs', max_length=20)),
+ ('units', models.CharField(blank=True, default='pcs', help_text='Stock keeping units for this part', max_length=20)),
('buildable', models.BooleanField(default=False, help_text='Can this part be built from other parts?')),
+ ('consumable', models.BooleanField(default=True, help_text='Can this part be used to build other parts?')),
('trackable', models.BooleanField(default=False, help_text='Does this part have tracking for unique items?')),
('purchaseable', models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?')),
('salable', models.BooleanField(default=False, help_text='Can this part be sold to customers?')),
+ ('active', models.BooleanField(default=True, help_text='Is this part active?')),
('notes', models.TextField(blank=True)),
+ ('bom_checksum', models.CharField(blank=True, help_text='Stored BOM checksum', max_length=128)),
+ ('bom_checked_date', models.DateField(blank=True, null=True)),
],
options={
'verbose_name': 'Part',
@@ -53,8 +61,8 @@ class Migration(migrations.Migration):
name='PartAttachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('attachment', models.FileField(blank=True, null=True, upload_to=part.models.attach_file)),
- ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='part.Part')),
+ ('attachment', models.FileField(help_text='Select file to attach', upload_to=part.models.attach_file)),
+ ('comment', models.CharField(help_text='File comment', max_length=100)),
],
),
migrations.CreateModel(
@@ -63,7 +71,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(max_length=250)),
- ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='part.PartCategory')),
+ ('default_keywords', models.CharField(blank=True, help_text='Default keywords for parts in this category', max_length=250)),
],
options={
'verbose_name': 'Part Category',
@@ -71,63 +79,11 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
- name='SupplierPart',
+ name='PartStar',
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)),
- ('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)),
- ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part')),
- ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='company.Company')),
+ ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_users', to='part.Part')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_parts', to=settings.AUTH_USER_MODEL)),
],
),
- 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='part.SupplierPart')),
- ],
- ),
- migrations.AddField(
- model_name='part',
- name='category',
- field=models.ForeignKey(blank=True, help_text='Part category', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='parts', to='part.PartCategory'),
- ),
- migrations.AddField(
- 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='part.SupplierPart'),
- ),
- migrations.AddField(
- model_name='bomitem',
- name='part',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part'),
- ),
- migrations.AddField(
- model_name='bomitem',
- name='sub_part',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part'),
- ),
- migrations.AlterUniqueTogether(
- name='supplierpricebreak',
- unique_together=set([('part', 'quantity')]),
- ),
- migrations.AlterUniqueTogether(
- name='supplierpart',
- unique_together=set([('part', 'supplier', 'SKU')]),
- ),
- migrations.AlterUniqueTogether(
- name='bomitem',
- unique_together=set([('part', 'sub_part')]),
- ),
]
diff --git a/InvenTree/part/migrations/0002_auto_20190519_0004.py b/InvenTree/part/migrations/0002_auto_20190519_0004.py
new file mode 100644
index 0000000000..02df1a7fad
--- /dev/null
+++ b/InvenTree/part/migrations/0002_auto_20190519_0004.py
@@ -0,0 +1,77 @@
+# Generated by Django 2.2 on 2019-05-18 14:04
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('company', '0002_auto_20190519_0004'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('stock', '0001_initial'),
+ ('part', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='partcategory',
+ name='default_location',
+ field=models.ForeignKey(blank=True, help_text='Default location for parts in this category', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_categories', to='stock.StockLocation'),
+ ),
+ migrations.AddField(
+ model_name='partcategory',
+ name='parent',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='part.PartCategory'),
+ ),
+ migrations.AddField(
+ model_name='partattachment',
+ name='part',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='part.Part'),
+ ),
+ migrations.AddField(
+ model_name='part',
+ name='bom_checked_by',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='boms_checked', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='part',
+ name='category',
+ field=models.ForeignKey(blank=True, help_text='Part category', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='parts', to='part.PartCategory'),
+ ),
+ migrations.AddField(
+ model_name='part',
+ name='default_location',
+ field=models.ForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='stock.StockLocation'),
+ ),
+ migrations.AddField(
+ 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.AddField(
+ model_name='bomitem',
+ name='part',
+ field=models.ForeignKey(help_text='Select parent part', limit_choices_to={'active': True, 'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part'),
+ ),
+ migrations.AddField(
+ model_name='bomitem',
+ name='sub_part',
+ field=models.ForeignKey(help_text='Select part to be used in BOM', limit_choices_to={'active': True, 'consumable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='partstar',
+ unique_together={('part', 'user')},
+ ),
+ migrations.AlterUniqueTogether(
+ name='part',
+ unique_together={('name', 'variant')},
+ ),
+ migrations.AlterUniqueTogether(
+ name='bomitem',
+ unique_together={('part', 'sub_part')},
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0002_part_default_location.py b/InvenTree/part/migrations/0002_part_default_location.py
deleted file mode 100644
index c5a65d284d..0000000000
--- a/InvenTree/part/migrations/0002_part_default_location.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-22 11:53
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0001_initial'),
- ('part', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='part',
- name='default_location',
- field=models.ForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='stock.StockLocation'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0003_auto_20190412_2030.py b/InvenTree/part/migrations/0003_auto_20190412_2030.py
deleted file mode 100644
index 3439c09483..0000000000
--- a/InvenTree/part/migrations/0003_auto_20190412_2030.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-04-12 10:30
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0002_part_default_location'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='bomitem',
- name='part',
- field=models.ForeignKey(limit_choices_to={'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0004_bomitem_note.py b/InvenTree/part/migrations/0004_bomitem_note.py
deleted file mode 100644
index 69bea83175..0000000000
--- a/InvenTree/part/migrations/0004_bomitem_note.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-14 08:25
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0003_auto_20190412_2030'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='bomitem',
- name='note',
- field=models.CharField(blank=True, help_text='Item notes', max_length=100),
- ),
- ]
diff --git a/InvenTree/part/migrations/0005_part_consumable.py b/InvenTree/part/migrations/0005_part_consumable.py
deleted file mode 100644
index 4bb1056ae7..0000000000
--- a/InvenTree/part/migrations/0005_part_consumable.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-15 13:48
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0004_bomitem_note'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='part',
- name='consumable',
- field=models.BooleanField(default=False, help_text='Can this part be used to build other parts?'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0006_auto_20190416_2354.py b/InvenTree/part/migrations/0006_auto_20190416_2354.py
deleted file mode 100644
index f2b2f42398..0000000000
--- a/InvenTree/part/migrations/0006_auto_20190416_2354.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 2.2 on 2019-04-16 13:54
-
-import django.core.validators
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0005_part_consumable'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='bomitem',
- name='sub_part',
- field=models.ForeignKey(limit_choices_to={'consumable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part'),
- ),
- migrations.AlterField(
- model_name='part',
- name='consumable',
- field=models.BooleanField(default=True, help_text='Can this part be used to build other parts?'),
- ),
- migrations.AlterField(
- model_name='supplierpricebreak',
- name='quantity',
- field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(2)]),
- ),
- ]
diff --git a/InvenTree/part/migrations/0007_auto_20190417_0007.py b/InvenTree/part/migrations/0007_auto_20190417_0007.py
deleted file mode 100644
index 554a471132..0000000000
--- a/InvenTree/part/migrations/0007_auto_20190417_0007.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Generated by Django 2.2 on 2019-04-16 14:07
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0006_auto_20190416_2354'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='supplierpart',
- name='note',
- field=models.CharField(blank=True, help_text='Notes', max_length=100),
- ),
- migrations.AlterField(
- model_name='supplierpart',
- name='base_cost',
- field=models.DecimalField(decimal_places=3, default=0, help_text='Minimum charge (e.g. stocking fee)', max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- migrations.AlterField(
- model_name='supplierpart',
- name='description',
- field=models.CharField(blank=True, help_text='Supplier part description', max_length=250),
- ),
- migrations.AlterField(
- model_name='supplierpart',
- name='minimum',
- field=models.PositiveIntegerField(default=1, help_text='Minimum order quantity (MOQ)', validators=[django.core.validators.MinValueValidator(0)]),
- ),
- migrations.AlterField(
- model_name='supplierpart',
- name='multiple',
- field=models.PositiveIntegerField(default=1, help_text='Order multiple', validators=[django.core.validators.MinValueValidator(0)]),
- ),
- migrations.AlterField(
- model_name='supplierpart',
- name='packaging',
- field=models.CharField(blank=True, help_text='Part packaging', max_length=50),
- ),
- migrations.AlterField(
- model_name='supplierpart',
- name='single_price',
- field=models.DecimalField(decimal_places=3, default=0, help_text='Price for single quantity', max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- migrations.AlterField(
- model_name='supplierpricebreak',
- name='cost',
- field=models.DecimalField(decimal_places=3, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- ]
diff --git a/InvenTree/part/migrations/0008_auto_20190417_0013.py b/InvenTree/part/migrations/0008_auto_20190417_0013.py
deleted file mode 100644
index e7311e2be8..0000000000
--- a/InvenTree/part/migrations/0008_auto_20190417_0013.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-04-16 14:13
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0007_auto_20190417_0007'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='supplierpart',
- name='part',
- field=models.ForeignKey(limit_choices_to={'purchasable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0009_auto_20190417_0019.py b/InvenTree/part/migrations/0009_auto_20190417_0019.py
deleted file mode 100644
index 2a6eba4960..0000000000
--- a/InvenTree/part/migrations/0009_auto_20190417_0019.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-04-16 14:19
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0008_auto_20190417_0013'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='supplierpart',
- name='part',
- field=models.ForeignKey(limit_choices_to={'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0010_auto_20190417_0045.py b/InvenTree/part/migrations/0010_auto_20190417_0045.py
deleted file mode 100644
index 1040afc67c..0000000000
--- a/InvenTree/part/migrations/0010_auto_20190417_0045.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 2.2 on 2019-04-16 14:45
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0009_auto_20190417_0019'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='supplierpart',
- name='minimum',
- field=models.PositiveIntegerField(default=1, help_text='Minimum order quantity (MOQ)', validators=[django.core.validators.MinValueValidator(1)]),
- ),
- migrations.AlterField(
- model_name='supplierpart',
- name='multiple',
- field=models.PositiveIntegerField(default=1, help_text='Order multiple', validators=[django.core.validators.MinValueValidator(1)]),
- ),
- ]
diff --git a/InvenTree/part/migrations/0011_auto_20190428_0841.py b/InvenTree/part/migrations/0011_auto_20190428_0841.py
deleted file mode 100644
index 0146b6795a..0000000000
--- a/InvenTree/part/migrations/0011_auto_20190428_0841.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-04-27 22:41
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0010_auto_20190417_0045'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='supplierpart',
- name='supplier',
- field=models.ForeignKey(limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='company.Company'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0012_part_active.py b/InvenTree/part/migrations/0012_part_active.py
deleted file mode 100644
index 87929e2f85..0000000000
--- a/InvenTree/part/migrations/0012_part_active.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 2.2 on 2019-04-28 13:00
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0011_auto_20190428_0841'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='part',
- name='active',
- field=models.BooleanField(default=True, help_text='Is this part active?'),
- ),
- migrations.AddField(
- model_name='partattachment',
- name='comment',
- field=models.CharField(blank=True, help_text='File comment', max_length=100),
- ),
- ]
diff --git a/InvenTree/part/migrations/0013_auto_20190429_2229.py b/InvenTree/part/migrations/0013_auto_20190429_2229.py
deleted file mode 100644
index 9c339b18b1..0000000000
--- a/InvenTree/part/migrations/0013_auto_20190429_2229.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-29 12:29
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0012_part_active'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='part',
- name='URL',
- field=models.URLField(blank=True, help_text='Link to external URL'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0014_auto_20190502_2039.py b/InvenTree/part/migrations/0014_auto_20190502_2039.py
deleted file mode 100644
index c32c3afe32..0000000000
--- a/InvenTree/part/migrations/0014_auto_20190502_2039.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-05-02 10:39
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0013_auto_20190429_2229'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='part',
- name='URL',
- field=models.URLField(blank=True, help_text='Link to extenal URL'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0015_partcategory_default_location.py b/InvenTree/part/migrations/0015_partcategory_default_location.py
deleted file mode 100644
index 33a47cc440..0000000000
--- a/InvenTree/part/migrations/0015_partcategory_default_location.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 2.2 on 2019-05-04 08:57
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0013_remove_stockitem_uuid'),
- ('part', '0014_auto_20190502_2039'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='partcategory',
- name='default_location',
- field=models.ForeignKey(blank=True, help_text='Default location for parts in this category', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_categories', to='stock.StockLocation'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0016_partstar.py b/InvenTree/part/migrations/0016_partstar.py
deleted file mode 100644
index baa5c83d5b..0000000000
--- a/InvenTree/part/migrations/0016_partstar.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 2.2 on 2019-05-04 22:45
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('part', '0015_partcategory_default_location'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='PartStar',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_users', to='part.Part')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_parts', to=settings.AUTH_USER_MODEL)),
- ],
- ),
- ]
diff --git a/InvenTree/part/migrations/0017_auto_20190505_0848.py b/InvenTree/part/migrations/0017_auto_20190505_0848.py
deleted file mode 100644
index 90162132f9..0000000000
--- a/InvenTree/part/migrations/0017_auto_20190505_0848.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-05-04 22:48
-
-from django.conf import settings
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('part', '0016_partstar'),
- ]
-
- operations = [
- migrations.AlterUniqueTogether(
- name='partstar',
- unique_together={('part', 'user')},
- ),
- ]
diff --git a/InvenTree/part/migrations/0018_auto_20190505_2231.py b/InvenTree/part/migrations/0018_auto_20190505_2231.py
deleted file mode 100644
index 32aa1e3a1b..0000000000
--- a/InvenTree/part/migrations/0018_auto_20190505_2231.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 2.2 on 2019-05-05 12:31
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0017_auto_20190505_0848'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='bomitem',
- name='part',
- field=models.ForeignKey(limit_choices_to={'active': True, 'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part'),
- ),
- migrations.AlterField(
- model_name='bomitem',
- name='sub_part',
- field=models.ForeignKey(limit_choices_to={'active': True, 'consumable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part'),
- ),
- ]
diff --git a/InvenTree/part/migrations/0020_auto_20190510_2022.py b/InvenTree/part/migrations/0020_auto_20190510_2022.py
deleted file mode 100644
index 8bf6db8f18..0000000000
--- a/InvenTree/part/migrations/0020_auto_20190510_2022.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Generated by Django 2.2 on 2019-05-10 10:22
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0019_auto_20190508_2332'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='part',
- name='variant',
- field=models.CharField(blank=True, help_text='Part variant or revision code', max_length=32),
- ),
- migrations.AlterField(
- model_name='part',
- name='name',
- field=models.CharField(help_text='Part name', max_length=100),
- ),
- migrations.AlterUniqueTogether(
- name='part',
- unique_together={('name', 'variant')},
- ),
- ]
diff --git a/InvenTree/part/migrations/0021_auto_20190510_2220.py b/InvenTree/part/migrations/0021_auto_20190510_2220.py
deleted file mode 100644
index 294bd112ae..0000000000
--- a/InvenTree/part/migrations/0021_auto_20190510_2220.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-05-10 12:20
-
-import InvenTree.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0020_auto_20190510_2022'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='part',
- name='name',
- field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name]),
- ),
- ]
diff --git a/InvenTree/part/migrations/0022_auto_20190512_1246.py b/InvenTree/part/migrations/0022_auto_20190512_1246.py
deleted file mode 100644
index 40f4d0dd4c..0000000000
--- a/InvenTree/part/migrations/0022_auto_20190512_1246.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Generated by Django 2.2 on 2019-05-12 02:46
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('part', '0021_auto_20190510_2220'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='part',
- name='bom_checked_by',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='boms_checked', to=settings.AUTH_USER_MODEL),
- ),
- migrations.AddField(
- model_name='part',
- name='bom_checked_date',
- field=models.DateField(blank=True, null=True),
- ),
- migrations.AddField(
- model_name='part',
- name='bom_checksum',
- field=models.CharField(blank=True, help_text='Stored BOM checksum', max_length=128),
- ),
- ]
diff --git a/InvenTree/part/migrations/0023_part_keywords.py b/InvenTree/part/migrations/0023_part_keywords.py
deleted file mode 100644
index 4752d80740..0000000000
--- a/InvenTree/part/migrations/0023_part_keywords.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-05-14 07:15
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0022_auto_20190512_1246'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='part',
- name='keywords',
- field=models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250),
- ),
- ]
diff --git a/InvenTree/part/migrations/0024_partcategory_default_keywords.py b/InvenTree/part/migrations/0024_partcategory_default_keywords.py
deleted file mode 100644
index 317d982f7d..0000000000
--- a/InvenTree/part/migrations/0024_partcategory_default_keywords.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-05-14 07:27
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0023_part_keywords'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='partcategory',
- name='default_keywords',
- field=models.CharField(blank=True, help_text='Default keywords for parts in this category', max_length=250),
- ),
- ]
diff --git a/InvenTree/part/migrations/0025_auto_20190515_0012.py b/InvenTree/part/migrations/0025_auto_20190515_0012.py
deleted file mode 100644
index aaeb8ea1a3..0000000000
--- a/InvenTree/part/migrations/0025_auto_20190515_0012.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Generated by Django 2.2 on 2019-05-14 14:12
-
-import InvenTree.validators
-import django.core.validators
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('part', '0024_partcategory_default_keywords'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='bomitem',
- name='overage',
- field=models.CharField(blank=True, help_text='Estimated build wastage quantity (absolute or percentage)', max_length=24, validators=[InvenTree.validators.validate_overage]),
- ),
- migrations.AlterField(
- model_name='bomitem',
- name='note',
- field=models.CharField(blank=True, help_text='BOM item notes', max_length=100),
- ),
- migrations.AlterField(
- model_name='bomitem',
- name='part',
- field=models.ForeignKey(help_text='Select parent part', limit_choices_to={'active': True, 'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part'),
- ),
- migrations.AlterField(
- model_name='bomitem',
- name='quantity',
- field=models.PositiveIntegerField(default=1, help_text='BOM quantity for this BOM item', validators=[django.core.validators.MinValueValidator(0)]),
- ),
- migrations.AlterField(
- model_name='bomitem',
- name='sub_part',
- field=models.ForeignKey(help_text='Select part to be used in BOM', limit_choices_to={'active': True, 'consumable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part'),
- ),
- migrations.AlterField(
- model_name='supplierpart',
- name='URL',
- field=models.URLField(blank=True, help_text='URL for external supplier part link'),
- ),
- ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index f4673dc309..d8b7529b23 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -7,8 +7,6 @@ from __future__ import unicode_literals
import os
-import math
-
import tablib
from django.utils.translation import gettext_lazy as _
@@ -32,7 +30,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 +316,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',
@@ -562,6 +561,203 @@ class Part(models.Model):
""" Return the number of supplier parts available for this part """
return self.supplier_parts.count()
+ @property
+ def min_single_price(self):
+ return self.get_min_supplier_price(1)
+
+ @property
+ def max_single_price(self):
+ return self.get_max_supplier_price(1)
+
+ @property
+ def min_bom_price(self):
+ return self.get_min_bom_price(1)
+
+ @property
+ def max_bom_price(self):
+ return self.get_max_bom_price(1)
+
+ @property
+ def has_pricing_info(self):
+ """ Return true if there is pricing information for this part """
+ return self.get_min_price() is not None
+
+ @property
+ def has_complete_bom_pricing(self):
+ """ Return true if there is pricing information for each item in the BOM. """
+
+ for item in self.bom_items.all():
+ if not item.sub_part.has_pricing_info:
+ return False
+
+ return True
+
+ @property
+ def single_price_info(self):
+ """ Return a simplified pricing string for this part at single quantity """
+
+ return self.get_price_info()
+
+ def get_price_info(self, quantity=1, buy=True, bom=True):
+ """ Return a simplified pricing string for this part
+
+ Args:
+ quantity: Number of units to calculate price for
+ buy: Include supplier pricing (default = True)
+ bom: Include BOM pricing (default = True)
+ """
+
+ min_price = self.get_min_price(quantity, buy, bom)
+ max_price = self.get_max_price(quantity, buy, bom)
+
+ if min_price is None:
+ return None
+
+ if min_price == max_price:
+ return min_price
+
+ return "{a} to {b}".format(a=min_price, b=max_price)
+
+ def get_min_supplier_price(self, quantity=1):
+ """ Return the minimum price of this part from all available suppliers.
+
+ Args:
+ quantity: Number of units we wish to purchase (default = 1)
+
+ Returns:
+ Numerical price if pricing is available, else None
+ """
+
+ min_price = None
+
+ for supplier_part in self.supplier_parts.all():
+ supplier_price = supplier_part.get_price(quantity)
+
+ if supplier_price is None:
+ continue
+
+ if min_price is None or supplier_price < min_price:
+ min_price = supplier_price
+
+ if min_price is None:
+ return None
+ else:
+ return min_price
+
+ def get_max_supplier_price(self, quantity=1):
+ """ Return the maximum price of this part from all available suppliers.
+
+ Args:
+ quantity: Number of units we wish to purchase (default = 1)
+
+ Returns:
+ Numerical price if pricing is available, else None
+ """
+
+ max_price = None
+
+ for supplier_part in self.supplier_parts.all():
+ supplier_price = supplier_part.get_price(quantity)
+
+ if supplier_price is None:
+ continue
+
+ if max_price is None or supplier_price > max_price:
+ max_price = supplier_price
+
+ if max_price is None:
+ return None
+ else:
+ return max_price
+
+ def get_min_bom_price(self, quantity=1):
+ """ Return the minimum price of the BOM for this part.
+ Adds the minimum price for all components in the BOM.
+
+ Note: If the BOM contains items without pricing information,
+ these items cannot be included in the BOM!
+ """
+
+ min_price = None
+
+ for item in self.bom_items.all():
+ price = item.sub_part.get_min_price(quantity * item.quantity)
+
+ if price is None:
+ continue
+
+ if min_price is None:
+ min_price = 0
+
+ min_price += price
+
+ return min_price
+
+ def get_max_bom_price(self, quantity=1):
+ """ Return the maximum price of the BOM for this part.
+ Adds the maximum price for all components in the BOM.
+
+ Note: If the BOM contains items without pricing information,
+ these items cannot be included in the BOM!
+ """
+
+ max_price = None
+
+ for item in self.bom_items.all():
+ price = item.sub_part.get_max_price(quantity * item.quantity)
+
+ if price is None:
+ continue
+
+ if max_price is None:
+ max_price = 0
+
+ max_price += price
+
+ return max_price
+
+ def get_min_price(self, quantity=1, buy=True, bom=True):
+ """ Return the minimum price for this part. This price can be either:
+
+ - Supplier price (if purchased from suppliers)
+ - BOM price (if built from other parts)
+
+ Returns:
+ Minimum of the supplier price or BOM price. If no pricing available, returns None
+ """
+
+ buy_price = self.get_min_supplier_price(quantity) if buy else None
+ bom_price = self.get_min_bom_price(quantity) if bom else None
+
+ if buy_price is None:
+ return bom_price
+
+ if bom_price is None:
+ return buy_price
+
+ return min(buy_price, bom_price)
+
+ def get_max_price(self, quantity=1, buy=True, bom=True):
+ """ Return the maximum price for this part. This price can be either:
+
+ - Supplier price (if purchsed from suppliers)
+ - BOM price (if built from other parts)
+
+ Returns:
+ Maximum of the supplier price or BOM price. If no pricing available, returns None
+ """
+
+ buy_price = self.get_max_supplier_price(quantity) if buy else None
+ bom_price = self.get_max_bom_price(quantity) if bom else None
+
+ if buy_price is None:
+ return bom_price
+
+ if bom_price is None:
+ return buy_price
+
+ return max(buy_price, bom_price)
+
def deepCopy(self, other, **kwargs):
""" Duplicates non-field data from another part.
Does not alter the normal fields of this part,
@@ -641,10 +837,10 @@ class PartAttachment(models.Model):
part = models.ForeignKey(Part, on_delete=models.CASCADE,
related_name='attachments')
- attachment = models.FileField(upload_to=attach_file, null=True, blank=True,
+ attachment = models.FileField(upload_to=attach_file,
help_text='Select file to attach')
- comment = models.CharField(max_length=100, blank=True, help_text='File comment')
+ comment = models.CharField(max_length=100, help_text='File comment')
@property
def basename(self):
@@ -800,164 +996,7 @@ 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
- single_price: Default price for a single unit
- 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')
-
- single_price = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Price for single quantity')
-
- 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
-
- def get_price(self, quantity, moq=True, multiples=True):
- """ Calculate the supplier price based on quantity price breaks.
-
- - If no price breaks available, use the single_price field
- - 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
- """
-
- # Order multiples
- if multiples:
- quantity = int(math.ceil(quantity / self.multipe) * self.multiple)
-
- # Minimum ordering requirement
- if moq and self.minimum > quantity:
- quantity = self.minimum
-
- pb_found = False
- pb_quantity = -1
- pb_cost = 0.0
-
- for pb in self.price_breaks.all():
- # Ignore this pricebreak!
- 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
-
- # No appropriate price-break found - use the single cost!
- if pb_found:
- cost = pb_cost * quantity
- else:
- cost = self.single_price * quantity
-
- return cost + self.base_cost
-
- 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='price_breaks')
-
- # At least 2 units are required for a 'price break' - Otherwise, just use single-price!
- quantity = models.PositiveIntegerField(validators=[MinValueValidator(2)])
-
- cost = models.DecimalField(max_digits=10, decimal_places=3, validators=[MinValueValidator(0)])
-
- 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)
+ def price_info(self):
+ """ Return the price for this item in the BOM """
+ return self.sub_part.get_price_info(self.quantity)
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 87ca59c13b..593f7003f9 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -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
@@ -34,6 +34,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True)
image_url = serializers.CharField(source='get_image_url', read_only=True)
+ single_price_info = serializers.CharField(read_only=True)
class Meta:
model = Part
@@ -43,6 +44,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
'full_name',
'description',
'available_stock',
+ 'single_price_info',
'image_url',
]
@@ -106,6 +108,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
+ price_info = serializers.CharField(read_only=True)
class Meta:
model = BomItem
@@ -116,46 +119,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
'sub_part',
'sub_part_detail',
'quantity',
+ 'price_info',
'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'
- ]
diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html
index 3990bfab6c..bff082b6ae 100644
--- a/InvenTree/part/templates/part/bom.html
+++ b/InvenTree/part/templates/part/bom.html
@@ -11,6 +11,14 @@
Bill of Materials
+{% if part.has_complete_bom_pricing == False %}
+
+ The BOM for {{ part.full_name }} does not have complete pricing information
+
+{% endif %}
+
+ Single BOM Price: {{ part.min_bom_price }} to {{ part.max_bom_price }}
+
{% if part.bom_checked_date %}
{% if part.is_bom_valid %}
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index ea3bbc7617..5e4f85d238 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -32,10 +32,13 @@
{{ part.description }}
- {% include "qr_button.html" %}
+ {% include "qr_button.html" %}
+
@@ -82,6 +85,25 @@
{{ part.allocation_count }} |
{% endif %}
+ {% if part.supplier_count > 0 %}
+
+
+ Price
+ |
+
+ {% if part.min_single_price %}
+ {% if part.min_single_price == part.max_single_price %}
+ {{ part.min_single_price }}
+ {% else %}
+ {{ part.min_single_price }} to {{ part.max_single_price }}
+ {% endif %}
+ from {{ part.supplier_count }} suppliers.
+ {% else %}
+ No pricing data avilable
+ {% endif %}
+ |
+
+ {% endif %}
@@ -122,6 +144,15 @@
);
});
+ $("#price-button").click(function() {
+ launchModalForm(
+ "{% url 'part-pricing' part.id %}",
+ {
+ submit_text: 'Calculate',
+ }
+ );
+ });
+
$("#toggle-starred").click(function() {
toggleStar({
part: {{ part.id }},
diff --git a/InvenTree/part/templates/part/part_pricing.html b/InvenTree/part/templates/part/part_pricing.html
new file mode 100644
index 0000000000..6d82549bb2
--- /dev/null
+++ b/InvenTree/part/templates/part/part_pricing.html
@@ -0,0 +1,88 @@
+{% extends "modal_form.html" %}
+
+{% block pre_form_content %}
+
+
+Calculate pricing information for {{ part }}.
+
+
+
Quantity
+
+
+ Part |
+ {{ part }} |
+
+
+ Quantity |
+ {{ quantity }} |
+
+
+ {% if part.supplier_count > 0 %}
+
Supplier Pricing
+
+ {% if min_total_buy_price %}
+
+ Unit Cost |
+ Min: {{ min_unit_buy_price }} |
+ Max: {{ max_unit_buy_price }} |
+
+ {% if quantity > 1 %}
+
+ Total Cost |
+ Min: {{ min_total_buy_price }} |
+ Max: {{ max_total_buy_price }} |
+
+ {% endif %}
+ {% else %}
+
+
+ No supplier pricing available
+ |
+
+ {% endif %}
+
+ {% endif %}
+
+ {% if part.bom_count > 0 %}
+
BOM Pricing
+
+ {% if min_total_bom_price %}
+
+ Unit Cost |
+ Min: {{ min_unit_bom_price }} |
+ Max: {{ max_unit_bom_price }} |
+
+ {% if quantity > 1 %}
+
+ Total Cost |
+ Min: {{ min_total_bom_price }} |
+ Max: {{ max_total_bom_price }} |
+
+ {% endif %}
+ {% if part.has_complete_bom_pricing == False %}
+
+
+ Note: BOM pricing is incomplete for this part
+ |
+
+ {% endif %}
+ {% else %}
+
+
+ No BOM pricing available
+ |
+
+ {% endif %}
+
+ {% endif %}
+
+{% if min_unit_buy_price or min_unit_bom_price %}
+{% else %}
+
+ No pricing information is available for this part.
+
+{% endif %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index 0671240e73..900ebe8127 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -12,19 +12,6 @@ from django.conf.urls import url, include
from . import views
-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
\d+)/', include(supplier_part_detail_urls)),
-]
-
part_attachment_urls = [
url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
url(r'^(?P\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
@@ -37,6 +24,7 @@ part_detail_urls = [
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'),
+ url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 55482d3284..d5824b2dab 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -12,12 +12,12 @@ from django.views.generic import DetailView, ListView
from django.forms.models import model_to_dict
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 match_part_names
+from company.models import SupplierPart
+
from . import forms as part_forms
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
@@ -551,6 +551,80 @@ class PartDelete(AjaxDeleteView):
}
+class PartPricing(AjaxView):
+ """ View for inspecting part pricing information """
+
+ model = Part
+ ajax_template_name = "part/part_pricing.html"
+ ajax_form_title = "Part Pricing"
+ form_class = part_forms.PartPriceForm
+
+ def get_part(self):
+ try:
+ return Part.objects.get(id=self.kwargs['pk'])
+ except Part.DoesNotExist:
+ return None
+
+ def get_pricing(self, quantity=1):
+
+ part = self.get_part()
+
+ ctx = {
+ 'part': part,
+ 'quantity': quantity
+ }
+
+ if part is None:
+ return ctx
+
+ # Supplier pricing information
+ if part.supplier_count > 0:
+ min_buy_price = part.get_min_supplier_price(quantity)
+ max_buy_price = part.get_max_supplier_price(quantity)
+
+ if min_buy_price:
+ ctx['min_total_buy_price'] = min_buy_price
+ ctx['min_unit_buy_price'] = min_buy_price / quantity
+
+ if max_buy_price:
+ ctx['max_total_buy_price'] = max_buy_price
+ ctx['max_unit_buy_price'] = max_buy_price / quantity
+
+ # BOM pricing information
+ if part.bom_count > 0:
+
+ min_bom_price = part.get_min_bom_price(quantity)
+ max_bom_price = part.get_max_bom_price(quantity)
+
+ if min_bom_price:
+ ctx['min_total_bom_price'] = min_bom_price
+ ctx['min_unit_bom_price'] = min_bom_price / quantity
+
+ if max_bom_price:
+ ctx['max_total_bom_price'] = max_bom_price
+ ctx['max_unit_bom_price'] = max_bom_price / quantity
+
+ return ctx
+
+ def get(self, request, *args, **kwargs):
+
+ return self.renderJsonResponse(request, self.form_class(), context=self.get_pricing())
+
+ def post(self, request, *args, **kwargs):
+
+ try:
+ quantity = int(self.request.POST.get('quantity', 1))
+ except ValueError:
+ quantity = 1
+
+ # Always mark the form as 'invalid' (the user may wish to keep getting pricing data)
+ data = {
+ 'form_valid': False,
+ }
+
+ return self.renderJsonResponse(request, self.form_class(), data=data, context=self.get_pricing(quantity))
+
+
class CategoryDetail(DetailView):
""" Detail view for PartCategory """
model = PartCategory
@@ -731,81 +805,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'
diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css
index 59e7899891..ff7ac4e98b 100644
--- a/InvenTree/static/css/inventree.css
+++ b/InvenTree/static/css/inventree.css
@@ -3,15 +3,19 @@
}
.glyphicon {
- font-size: 20px;
+ font-size: 18px;
}
.glyphicon-small {
- font-size: 14px;
+ font-size: 12px;
}
.starred-part {
- color: #ffcc00;
+ color: #ffbb00;
+}
+
+.part-price {
+ color: rgb(13, 245, 25);
}
/* CSS overrides for treeview */
diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js
index 6ff81de4fc..6e00eabe67 100644
--- a/InvenTree/static/script/inventree/bom.js
+++ b/InvenTree/static/script/inventree/bom.js
@@ -151,6 +151,19 @@ function loadBomTable(table, options) {
}
}
);
+
+ cols.push({
+ field: 'price_info',
+ title: 'Price',
+ sortable: true,
+ formatter: function(value, row, index, field) {
+ if (value) {
+ return value;
+ } else {
+ return "No pricing information";
+ }
+ },
+ });
}
// Part notes
diff --git a/InvenTree/stock/migrations/0001_initial.py b/InvenTree/stock/migrations/0001_initial.py
index b4d15a7f64..2fc4e96608 100644
--- a/InvenTree/stock/migrations/0001_initial.py
+++ b/InvenTree/stock/migrations/0001_initial.py
@@ -1,6 +1,4 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-22 11:53
-from __future__ import unicode_literals
+# Generated by Django 2.2 on 2019-05-18 14:04
from django.conf import settings
import django.core.validators
@@ -14,8 +12,8 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('part', '0001_initial'),
('company', '0001_initial'),
+ ('part', '0001_initial'),
]
operations = [
@@ -25,30 +23,19 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('serial', models.PositiveIntegerField(blank=True, help_text='Serial number for this item', null=True)),
('URL', models.URLField(blank=True, max_length=125)),
- ('batch', models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100)),
- ('quantity', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(0)])),
- ('updated', models.DateField(auto_now=True)),
+ ('batch', models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100, null=True)),
+ ('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)])),
+ ('updated', models.DateField(auto_now=True, null=True)),
('stocktake_date', models.DateField(blank=True, null=True)),
('review_needed', models.BooleanField(default=False)),
- ('status', models.PositiveIntegerField(choices=[(10, 'OK'), (60, 'Destroyed'), (50, 'Attention needed'), (55, 'Damaged')], default=10, validators=[django.core.validators.MinValueValidator(0)])),
- ('notes', models.TextField(blank=True)),
+ ('delete_on_deplete', models.BooleanField(default=True, help_text='Delete this Stock Item when stock is depleted')),
+ ('status', models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed')], default=10, validators=[django.core.validators.MinValueValidator(0)])),
+ ('notes', models.CharField(blank=True, help_text='Stock Item Notes', max_length=250)),
('infinite', models.BooleanField(default=False)),
('belongs_to', models.ForeignKey(blank=True, help_text='Is this item installed in another item?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owned_parts', to='stock.StockItem')),
('customer', models.ForeignKey(blank=True, help_text='Item assigned to customer?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stockitems', to='company.Company')),
],
),
- migrations.CreateModel(
- name='StockItemTracking',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('date', models.DateField(auto_now_add=True)),
- ('title', models.CharField(max_length=250)),
- ('notes', models.TextField(blank=True)),
- ('system', models.BooleanField(default=False)),
- ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracking_info', to='stock.StockItem')),
- ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
- ],
- ),
migrations.CreateModel(
name='StockLocation',
fields=[
@@ -59,34 +46,44 @@ class Migration(migrations.Migration):
],
options={
'abstract': False,
+ 'unique_together': {('name', 'parent')},
},
),
+ migrations.CreateModel(
+ name='StockItemTracking',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('date', models.DateTimeField(auto_now_add=True)),
+ ('title', models.CharField(max_length=250)),
+ ('notes', models.TextField(blank=True)),
+ ('system', models.BooleanField(default=False)),
+ ('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)])),
+ ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracking_info', to='stock.StockItem')),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
migrations.AddField(
model_name='stockitem',
name='location',
- field=models.ForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='items', to='stock.StockLocation'),
+ field=models.ForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='stock_items', to='stock.StockLocation'),
),
migrations.AddField(
model_name='stockitem',
name='part',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='part.Part'),
+ field=models.ForeignKey(help_text='Base part', on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='part.Part'),
),
migrations.AddField(
model_name='stockitem',
name='stocktake_user',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stocktake_stock', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='stockitem',
name='supplier_part',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='part.SupplierPart'),
- ),
- migrations.AlterUniqueTogether(
- name='stocklocation',
- unique_together=set([('name', 'parent')]),
+ 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'),
),
migrations.AlterUniqueTogether(
name='stockitem',
- unique_together=set([('part', 'serial')]),
+ unique_together={('part', 'serial')},
),
]
diff --git a/InvenTree/stock/migrations/0002_auto_20180430_1218.py b/InvenTree/stock/migrations/0002_auto_20180430_1218.py
deleted file mode 100644
index a3ed70090f..0000000000
--- a/InvenTree/stock/migrations/0002_auto_20180430_1218.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-04-30 12:18
-from __future__ import unicode_literals
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='quantity',
- field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0003_auto_20180510_1042.py b/InvenTree/stock/migrations/0003_auto_20180510_1042.py
deleted file mode 100644
index dfa34c6b5c..0000000000
--- a/InvenTree/stock/migrations/0003_auto_20180510_1042.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-05-10 10:42
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0002_auto_20180430_1218'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitemtracking',
- name='date',
- field=models.DateTimeField(auto_now_add=True),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0004_auto_20190412_2030.py b/InvenTree/stock/migrations/0004_auto_20190412_2030.py
deleted file mode 100644
index d55dbc6fff..0000000000
--- a/InvenTree/stock/migrations/0004_auto_20190412_2030.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-04-12 10:30
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0003_auto_20180510_1042'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='status',
- field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0005_stockitemtracking_quantity.py b/InvenTree/stock/migrations/0005_stockitemtracking_quantity.py
deleted file mode 100644
index f654486075..0000000000
--- a/InvenTree/stock/migrations/0005_stockitemtracking_quantity.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-04-12 14:09
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0004_auto_20190412_2030'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='stockitemtracking',
- name='quantity',
- field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)]),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0006_stockitem_uuid.py b/InvenTree/stock/migrations/0006_stockitem_uuid.py
deleted file mode 100644
index 4d358e7201..0000000000
--- a/InvenTree/stock/migrations/0006_stockitem_uuid.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-04-12 15:06
-
-from django.db import migrations, models
-import uuid
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0005_stockitemtracking_quantity'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='stockitem',
- name='uuid',
- field=models.UUIDField(blank=True, default=uuid.uuid4, editable=False),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0007_auto_20190417_1812.py b/InvenTree/stock/migrations/0007_auto_20190417_1812.py
deleted file mode 100644
index 8b9ec638e3..0000000000
--- a/InvenTree/stock/migrations/0007_auto_20190417_1812.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-17 08:12
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0006_stockitem_uuid'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='notes',
- field=models.CharField(blank=True, help_text='Stock Item Notes', max_length=250),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0008_auto_20190417_1819.py b/InvenTree/stock/migrations/0008_auto_20190417_1819.py
deleted file mode 100644
index 46659abd66..0000000000
--- a/InvenTree/stock/migrations/0008_auto_20190417_1819.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 2.2 on 2019-04-17 08:19
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0007_auto_20190417_1812'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='stocktake_user',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stocktake_stock', to=settings.AUTH_USER_MODEL),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0009_auto_20190428_0841.py b/InvenTree/stock/migrations/0009_auto_20190428_0841.py
deleted file mode 100644
index 4e22dad76b..0000000000
--- a/InvenTree/stock/migrations/0009_auto_20190428_0841.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-04-27 22:41
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0008_auto_20190417_1819'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='location',
- field=models.ForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='stock_items', to='stock.StockLocation'),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0010_auto_20190501_2344.py b/InvenTree/stock/migrations/0010_auto_20190501_2344.py
deleted file mode 100644
index 61ea730b03..0000000000
--- a/InvenTree/stock/migrations/0010_auto_20190501_2344.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-05-01 13:44
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0009_auto_20190428_0841'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='batch',
- field=models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100, null=True),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0011_auto_20190502_0041.py b/InvenTree/stock/migrations/0011_auto_20190502_0041.py
deleted file mode 100644
index b540421de5..0000000000
--- a/InvenTree/stock/migrations/0011_auto_20190502_0041.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-05-01 14:41
-
-from django.db import migrations, models
-import uuid
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0010_auto_20190501_2344'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='uuid',
- field=models.UUIDField(blank=True, default=uuid.uuid4, editable=False, help_text='Unique ID for the StockItem'),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0012_auto_20190502_0058.py b/InvenTree/stock/migrations/0012_auto_20190502_0058.py
deleted file mode 100644
index 1c86926fcd..0000000000
--- a/InvenTree/stock/migrations/0012_auto_20190502_0058.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-05-01 14:58
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0011_auto_20190502_0041'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='part',
- field=models.ForeignKey(help_text='Base part', on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='part.Part'),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0013_remove_stockitem_uuid.py b/InvenTree/stock/migrations/0013_remove_stockitem_uuid.py
deleted file mode 100644
index 79184862ed..0000000000
--- a/InvenTree/stock/migrations/0013_remove_stockitem_uuid.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 2.2 on 2019-05-02 10:39
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0012_auto_20190502_0058'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='stockitem',
- name='uuid',
- ),
- ]
diff --git a/InvenTree/stock/migrations/0014_auto_20190508_2332.py b/InvenTree/stock/migrations/0014_auto_20190508_2332.py
deleted file mode 100644
index 829bd34df8..0000000000
--- a/InvenTree/stock/migrations/0014_auto_20190508_2332.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2 on 2019-05-08 13:32
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0013_remove_stockitem_uuid'),
- ]
-
- 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='part.SupplierPart'),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0015_stockitem_delete_on_deplete.py b/InvenTree/stock/migrations/0015_stockitem_delete_on_deplete.py
deleted file mode 100644
index 29631b94d8..0000000000
--- a/InvenTree/stock/migrations/0015_stockitem_delete_on_deplete.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-05-09 12:59
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0014_auto_20190508_2332'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='stockitem',
- name='delete_on_deplete',
- field=models.BooleanField(default=True, help_text='Delete this Stock Item when stock is depleted'),
- ),
- ]
diff --git a/InvenTree/stock/migrations/0016_auto_20190512_2119.py b/InvenTree/stock/migrations/0016_auto_20190512_2119.py
deleted file mode 100644
index 582e68d277..0000000000
--- a/InvenTree/stock/migrations/0016_auto_20190512_2119.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-05-12 11:19
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('stock', '0015_stockitem_delete_on_deplete'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='stockitem',
- name='updated',
- field=models.DateField(auto_now=True, null=True),
- ),
- ]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index 950d6237dd..fcb1622f27 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -188,7 +188,7 @@ class StockItem(models.Model):
part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='locations', help_text='Base part')
- supplier_part = models.ForeignKey('part.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
+ supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
help_text='Select a matching supplier part for this stock item')
location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,