mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
90915f8dc0
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,6 +26,7 @@ var/
|
||||
*.log
|
||||
local_settings.py
|
||||
*.sqlite3
|
||||
*.backup
|
||||
|
||||
# Sphinx files
|
||||
docs/_build
|
||||
|
@ -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)),
|
||||
|
||||
|
@ -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
|
||||
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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',
|
||||
),
|
||||
]
|
@ -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',
|
||||
),
|
||||
]
|
@ -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')},
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -36,6 +36,19 @@ InvenTree | Build - {{ build }}
|
||||
<td>Quantity</td>
|
||||
<td>{{ build.quantity }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BOM Price</td>
|
||||
<td>
|
||||
{% if bom_price %}
|
||||
{{ bom_price }}
|
||||
{% if build.part.has_complete_bom_pricing == False %}
|
||||
<span class='warning-msg'><i>BOM pricing is incomplete</i></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class='warning-msg'><i>No pricing information</i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 """
|
||||
|
@ -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)
|
||||
|
@ -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<pk>\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<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),
|
||||
|
||||
|
@ -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'
|
||||
]
|
||||
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -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')},
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
@ -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'
|
||||
]
|
||||
|
@ -10,7 +10,7 @@ InvenTree | {{ company.name }} - Parts
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h3>Supplier Part</h3>
|
||||
<p>{{ part.SKU }} - <a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></p>
|
||||
<p><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a> - {{ part.SKU }}</p>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<h3>
|
||||
@ -59,29 +59,46 @@ InvenTree | {{ company.name }} - Parts
|
||||
|
||||
<div class='col-sm-6'>
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr><th colspan='2'>Pricing</th></tr>
|
||||
<tr><td>Single Price</td><td>{{ part.single_price }}</td></tr>
|
||||
{% if part.multiple > 1 %}
|
||||
<tr>
|
||||
<th colspan='2'>Pricing</th>
|
||||
</tr>
|
||||
<tr><td>Order Multiple</td><td>{{ part.multiple }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.base_cost > 0 %}
|
||||
<tr><td>Base Price (Flat Fee)</td><td>{{ part.base_cost }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.minimum > 1 %}
|
||||
<tr><td>Minimum Order Quantity</td><td>{{ part.minimum }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.price_breaks.all %}
|
||||
<tr><th colspan='2'>Price Breaks</th></tr>
|
||||
<tr>
|
||||
<th>Price Breaks</th>
|
||||
<th>
|
||||
<div style='float: right;'>
|
||||
<button class='btn btn-primary' id='new-price-break' type='button'>New Price Break</button>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
{% if part.price_breaks.all %}
|
||||
{% for pb in part.price_breaks.all %}
|
||||
<tr>
|
||||
<td>{{ pb.quantity }}</td>
|
||||
<td>{{ pb.cost }}</td>
|
||||
<td>{{ pb.cost }}
|
||||
<div class='btn-group' style='float: right;'>
|
||||
<button title='Edit Price Break' class='btn btn-primary pb-edit-button btn-sm' type='button' url="{% url 'price-break-edit' pb.id %}"><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>
|
||||
<button title='Delete Price Break' class='btn btn-danger pb-delete-button btn-sm' type='button' url="{% url 'price-break-delete' pb.id %}"><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan='2'>
|
||||
<span class='warning-msg'><i>No price breaks have been added for this part</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
@ -89,9 +106,7 @@ InvenTree | {{ company.name }} - Parts
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<button class='btn btn-primary' type='button'>New Price Break</button>
|
||||
</div>
|
||||
|
||||
|
||||
{% 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 %}
|
@ -36,3 +36,23 @@ company_urls = [
|
||||
# Redirect any other patterns
|
||||
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'),
|
||||
]
|
||||
|
||||
price_break_urls = [
|
||||
url('^new/', views.PriceBreakCreate.as_view(), name='price-break-create'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/edit/', views.PriceBreakEdit.as_view(), name='price-break-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
|
||||
]
|
||||
|
||||
supplier_part_detail_urls = [
|
||||
url(r'edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
|
||||
url(r'delete/?', views.SupplierPartDelete.as_view(), name='supplier-part-delete'),
|
||||
|
||||
url('^.*$', views.SupplierPartDetail.as_view(), name='supplier-part-detail'),
|
||||
]
|
||||
|
||||
supplier_part_urls = [
|
||||
url(r'^new/?', views.SupplierPartCreate.as_view(), name='supplier-part-create'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
|
||||
]
|
||||
|
@ -8,12 +8,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'
|
||||
|
@ -5,6 +5,5 @@ It includes models for:
|
||||
|
||||
- PartCategory
|
||||
- Part
|
||||
- SupplierPart
|
||||
- BomItem
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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<pk>\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<pk>\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<pk>\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<pk>\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<pk>\d+)/', BomDetail.as_view(), name='api-bom-detail'),
|
||||
|
@ -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'
|
||||
]
|
||||
|
@ -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')]),
|
||||
),
|
||||
]
|
||||
|
77
InvenTree/part/migrations/0002_auto_20190519_0004.py
Normal file
77
InvenTree/part/migrations/0002_auto_20190519_0004.py
Normal file
@ -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')},
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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?'),
|
||||
),
|
||||
]
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
@ -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')},
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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')},
|
||||
),
|
||||
]
|
@ -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]),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
@ -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'
|
||||
]
|
||||
|
@ -11,6 +11,14 @@
|
||||
|
||||
<h3>Bill of Materials</h3>
|
||||
|
||||
{% if part.has_complete_bom_pricing == False %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
The BOM for <i>{{ part.full_name }}</i> does not have complete pricing information
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class='panel panel-default'>
|
||||
Single BOM Price: {{ part.min_bom_price }} to {{ part.max_bom_price }}
|
||||
</div>
|
||||
{% if part.bom_checked_date %}
|
||||
{% if part.is_bom_valid %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
|
@ -32,10 +32,13 @@
|
||||
<p><i>{{ part.description }}</i></p>
|
||||
<p>
|
||||
<div class='btn-group'>
|
||||
{% include "qr_button.html" %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='toggle-starred' title='Star this part'>
|
||||
<span id='part-star-icon' class='starred-part glyphicon {% if starred %}glyphicon-star{% else %}glyphicon-star-empty{% endif %}'/>
|
||||
</button>
|
||||
{% include "qr_button.html" %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='price-button' title='Show pricing information'>
|
||||
<span id='part-price-icon' class='part-price glyphicon glyphicon-usd'/>
|
||||
</button>
|
||||
</div>
|
||||
</p>
|
||||
<table class='table table-condensed'>
|
||||
@ -82,6 +85,25 @@
|
||||
<td>{{ part.allocation_count }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.supplier_count > 0 %}
|
||||
<tr>
|
||||
<td>
|
||||
Price
|
||||
</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
<span class='warning-msg'><i>No pricing data avilable</i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -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 }},
|
||||
|
88
InvenTree/part/templates/part/part_pricing.html
Normal file
88
InvenTree/part/templates/part/part_pricing.html
Normal file
@ -0,0 +1,88 @@
|
||||
{% extends "modal_form.html" %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
<div class='alert alert-info alert-block'>
|
||||
Calculate pricing information for {{ part }}.
|
||||
</div>
|
||||
|
||||
<h4>Quantity</h4>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<tr>
|
||||
<td><b>Part</b></td>
|
||||
<td colspan='2'>{{ part }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Quantity</b></td>
|
||||
<td colspan='2'>{{ quantity }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if part.supplier_count > 0 %}
|
||||
<h4>Supplier Pricing</h4>
|
||||
<table class='table table-striped table-condensed'>
|
||||
{% if min_total_buy_price %}
|
||||
<tr>
|
||||
<td><b>Unit Cost</b></td>
|
||||
<td>Min: {{ min_unit_buy_price }}</td>
|
||||
<td>Max: {{ max_unit_buy_price }}</td>
|
||||
</tr>
|
||||
{% if quantity > 1 %}
|
||||
<tr>
|
||||
<td><b>Total Cost</b></td>
|
||||
<td>Min: {{ min_total_buy_price }}</td>
|
||||
<td>Max: {{ max_total_buy_price }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan='3'>
|
||||
<span class='warning-msg'><i>No supplier pricing available</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if part.bom_count > 0 %}
|
||||
<h4>BOM Pricing</h4>
|
||||
<table class='table table-striped table-condensed'>
|
||||
{% if min_total_bom_price %}
|
||||
<tr>
|
||||
<td><b>Unit Cost</b></td>
|
||||
<td>Min: {{ min_unit_bom_price }}</td>
|
||||
<td>Max: {{ max_unit_bom_price }}</td>
|
||||
</tr>
|
||||
{% if quantity > 1 %}
|
||||
<tr>
|
||||
<td><b>Total Cost</b></td>
|
||||
<td>Min: {{ min_total_bom_price }}</td>
|
||||
<td>Max: {{ max_total_bom_price }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.has_complete_bom_pricing == False %}
|
||||
<tr>
|
||||
<td colspan='3'>
|
||||
<span class='warning-msg'><i>Note: BOM pricing is incomplete for this part</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan='3'>
|
||||
<span class='warning-msg'><i>No BOM pricing available</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if min_unit_buy_price or min_unit_bom_price %}
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block'>
|
||||
No pricing information is available for this part.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
{% endblock %}
|
@ -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<pk>\d+)/', include(supplier_part_detail_urls)),
|
||||
]
|
||||
|
||||
part_attachment_urls = [
|
||||
url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
|
||||
@ -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'),
|
||||
|
@ -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'
|
||||
|
@ -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 */
|
||||
|
@ -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 "<span class='warning-msg'><i>No pricing information</i></span>";
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Part notes
|
||||
|
@ -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')},
|
||||
),
|
||||
]
|
||||
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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)]),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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',
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user