mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
2e2c51b271
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,6 +26,7 @@ var/
|
|||||||
*.log
|
*.log
|
||||||
local_settings.py
|
local_settings.py
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
*.backup
|
||||||
|
|
||||||
# Sphinx files
|
# Sphinx files
|
||||||
docs/_build
|
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 qr_code import urls as qr_code_urls
|
||||||
|
|
||||||
from company.urls import company_urls
|
from company.urls import company_urls
|
||||||
|
from company.urls import supplier_part_urls
|
||||||
|
from company.urls import price_break_urls
|
||||||
|
|
||||||
from part.urls import part_urls
|
from part.urls import part_urls
|
||||||
from part.urls import supplier_part_urls
|
|
||||||
|
|
||||||
from stock.urls import stock_urls
|
from stock.urls import stock_urls
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ apipatterns = [
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^part/', include(part_urls)),
|
url(r'^part/', include(part_urls)),
|
||||||
url(r'^supplier-part/', include(supplier_part_urls)),
|
url(r'^supplier-part/', include(supplier_part_urls)),
|
||||||
|
url(r'^price-break/', include(price_break_urls)),
|
||||||
|
|
||||||
url(r'^stock/', include(stock_urls)),
|
url(r'^stock/', include(stock_urls)),
|
||||||
|
|
||||||
|
@ -223,6 +223,7 @@ class AjaxCreateView(AjaxMixin, CreateView):
|
|||||||
|
|
||||||
super(CreateView, self).get(request, *args, **kwargs)
|
super(CreateView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
self.request = request
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
return self.renderJsonResponse(request, form)
|
return self.renderJsonResponse(request, form)
|
||||||
|
|
||||||
@ -233,6 +234,7 @@ class AjaxCreateView(AjaxMixin, CreateView):
|
|||||||
- If valid, save form
|
- If valid, save form
|
||||||
- Return status info (success / failure)
|
- Return status info (success / failure)
|
||||||
"""
|
"""
|
||||||
|
self.request = request
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
|
|
||||||
# Extra JSON data sent alongside form
|
# Extra JSON data sent alongside form
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Generated by Django 2.2 on 2019-05-18 14:04
|
||||||
# Generated by Django 1.11.12 on 2018-04-22 11:53
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -12,7 +11,9 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('part', '0001_initial'),
|
('part', '0002_auto_20190519_0004'),
|
||||||
|
('stock', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -20,14 +21,29 @@ class Migration(migrations.Migration):
|
|||||||
name='Build',
|
name='Build',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('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)])),
|
('quantity', models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)])),
|
||||||
('notes', models.TextField(blank=True)),
|
('status', models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status', validators=[django.core.validators.MinValueValidator(0)])),
|
||||||
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part')),
|
('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>Quantity</td>
|
||||||
<td>{{ build.quantity }}</td>
|
<td>{{ build.quantity }}</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -265,6 +265,16 @@ class BuildDetail(DetailView):
|
|||||||
template_name = 'build/detail.html'
|
template_name = 'build/detail.html'
|
||||||
context_object_name = 'build'
|
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):
|
class BuildAllocate(DetailView):
|
||||||
""" View for allocating parts to a Build """
|
""" View for allocating parts to a Build """
|
||||||
|
@ -2,10 +2,22 @@ from django.contrib import admin
|
|||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart
|
||||||
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
|
|
||||||
class CompanyAdmin(ImportExportModelAdmin):
|
class CompanyAdmin(ImportExportModelAdmin):
|
||||||
list_display = ('name', 'website', 'contact')
|
list_display = ('name', 'website', 'contact')
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPartAdmin(ImportExportModelAdmin):
|
||||||
|
list_display = ('part', 'supplier', 'SKU')
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPriceBreakAdmin(ImportExportModelAdmin):
|
||||||
|
list_display = ('part', 'quantity', 'cost')
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Company, CompanyAdmin)
|
admin.site.register(Company, CompanyAdmin)
|
||||||
|
admin.site.register(SupplierPart, SupplierPartAdmin)
|
||||||
|
admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin)
|
||||||
|
@ -9,10 +9,13 @@ from django_filters.rest_framework import DjangoFilterBackend
|
|||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
from rest_framework import generics, permissions
|
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 Company
|
||||||
|
from .models import SupplierPart, SupplierPriceBreak
|
||||||
|
|
||||||
from .serializers import CompanySerializer
|
from .serializers import CompanySerializer
|
||||||
|
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
|
||||||
|
|
||||||
|
|
||||||
class CompanyList(generics.ListCreateAPIView):
|
class CompanyList(generics.ListCreateAPIView):
|
||||||
@ -65,8 +68,86 @@ 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 = [
|
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'),
|
url(r'^(?P<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),
|
||||||
|
|
||||||
url(r'^.*$', CompanyList.as_view(), name='api-company-list'),
|
url(r'^.*$', CompanyList.as_view(), name='api-company-list'),
|
||||||
|
@ -8,6 +8,8 @@ from __future__ import unicode_literals
|
|||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart
|
||||||
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
|
|
||||||
class EditCompanyForm(HelperForm):
|
class EditCompanyForm(HelperForm):
|
||||||
@ -37,3 +39,37 @@ class CompanyImageForm(HelperForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'image'
|
'image'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EditSupplierPartForm(HelperForm):
|
||||||
|
""" Form for editing a SupplierPart object """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SupplierPart
|
||||||
|
fields = [
|
||||||
|
'part',
|
||||||
|
'supplier',
|
||||||
|
'SKU',
|
||||||
|
'description',
|
||||||
|
'manufacturer',
|
||||||
|
'MPN',
|
||||||
|
'URL',
|
||||||
|
'note',
|
||||||
|
'base_cost',
|
||||||
|
'multiple',
|
||||||
|
'minimum',
|
||||||
|
'packaging',
|
||||||
|
'lead_time'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EditPriceBreakForm(HelperForm):
|
||||||
|
""" Form for creating / editing a supplier price break """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SupplierPriceBreak
|
||||||
|
fields = [
|
||||||
|
'part',
|
||||||
|
'quantity',
|
||||||
|
'cost'
|
||||||
|
]
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Generated by Django 2.2 on 2019-05-18 14:04
|
||||||
# Generated by Django 1.11.12 on 2018-04-22 11:53
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import company.models
|
import company.models
|
||||||
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -18,15 +18,60 @@ class Migration(migrations.Migration):
|
|||||||
name='Company',
|
name='Company',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('name', models.CharField(help_text='Company name', max_length=100, unique=True)),
|
||||||
('description', models.CharField(max_length=500)),
|
('description', models.CharField(help_text='Description of the company', max_length=500)),
|
||||||
('website', models.URLField(blank=True, help_text='Company website URL')),
|
('website', models.URLField(blank=True, help_text='Company website URL')),
|
||||||
('address', models.CharField(blank=True, help_text='Company address', max_length=200)),
|
('address', models.CharField(blank=True, help_text='Company address', max_length=200)),
|
||||||
('phone', models.CharField(blank=True, max_length=50)),
|
('phone', models.CharField(blank=True, help_text='Contact phone number', max_length=50)),
|
||||||
('email', models.EmailField(blank=True, max_length=254)),
|
('email', models.EmailField(blank=True, help_text='Contact email address', max_length=254)),
|
||||||
('contact', models.CharField(blank=True, max_length=100)),
|
('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)),
|
('image', models.ImageField(blank=True, max_length=255, null=True, upload_to=company.models.rename_company_image)),
|
||||||
('notes', models.TextField(blank=True)),
|
('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
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import part.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('part', '0018_auto_20190505_2231'),
|
('company', '0001_initial'),
|
||||||
|
('part', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AddField(
|
||||||
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(
|
|
||||||
model_name='supplierpart',
|
model_name='supplierpart',
|
||||||
name='part',
|
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'),
|
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',
|
model_name='supplierpart',
|
||||||
name='supplier',
|
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'),
|
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 os
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -150,3 +154,174 @@ class Contact(models.Model):
|
|||||||
|
|
||||||
company = models.ForeignKey(Company, related_name='contacts',
|
company = models.ForeignKey(Company, related_name='contacts',
|
||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPart(models.Model):
|
||||||
|
""" Represents a unique part as provided by a Supplier
|
||||||
|
Each SupplierPart is identified by a MPN (Manufacturer Part Number)
|
||||||
|
Each SupplierPart is also linked to a Part object.
|
||||||
|
A Part may be available from multiple suppliers
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
part: Link to the master Part
|
||||||
|
supplier: Company that supplies this SupplierPart object
|
||||||
|
SKU: Stock keeping unit (supplier part number)
|
||||||
|
manufacturer: Manufacturer name
|
||||||
|
MPN: Manufacture part number
|
||||||
|
URL: Link to external website for this part
|
||||||
|
description: Descriptive notes field
|
||||||
|
note: Longer form note field
|
||||||
|
base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee"
|
||||||
|
multiple: Multiple that the part is provided in
|
||||||
|
minimum: MOQ (minimum order quantity) required for purchase
|
||||||
|
lead_time: Supplier lead time
|
||||||
|
packaging: packaging that the part is supplied in, e.g. "Reel"
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('part', 'supplier', 'SKU')
|
||||||
|
|
||||||
|
# This model was moved from the 'Part' app
|
||||||
|
db_table = 'part_supplierpart'
|
||||||
|
|
||||||
|
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||||
|
related_name='supplier_parts',
|
||||||
|
limit_choices_to={'purchaseable': True},
|
||||||
|
help_text='Select part',
|
||||||
|
)
|
||||||
|
|
||||||
|
supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
|
||||||
|
related_name='parts',
|
||||||
|
limit_choices_to={'is_supplier': True},
|
||||||
|
help_text='Select supplier',
|
||||||
|
)
|
||||||
|
|
||||||
|
SKU = models.CharField(max_length=100, help_text='Supplier stock keeping unit')
|
||||||
|
|
||||||
|
manufacturer = models.CharField(max_length=100, blank=True, help_text='Manufacturer')
|
||||||
|
|
||||||
|
MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
|
||||||
|
|
||||||
|
URL = models.URLField(blank=True, help_text='URL for external supplier part link')
|
||||||
|
|
||||||
|
description = models.CharField(max_length=250, blank=True, help_text='Supplier part description')
|
||||||
|
|
||||||
|
note = models.CharField(max_length=100, blank=True, help_text='Notes')
|
||||||
|
|
||||||
|
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Minimum charge (e.g. stocking fee)')
|
||||||
|
|
||||||
|
packaging = models.CharField(max_length=50, blank=True, help_text='Part packaging')
|
||||||
|
|
||||||
|
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Order multiple')
|
||||||
|
|
||||||
|
minimum = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Minimum order quantity (MOQ)')
|
||||||
|
|
||||||
|
lead_time = models.DurationField(blank=True, null=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manufacturer_string(self):
|
||||||
|
|
||||||
|
items = []
|
||||||
|
|
||||||
|
if self.manufacturer:
|
||||||
|
items.append(self.manufacturer)
|
||||||
|
if self.MPN:
|
||||||
|
items.append(self.MPN)
|
||||||
|
|
||||||
|
return ' | '.join(items)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_price_breaks(self):
|
||||||
|
return self.price_breaks.count() > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price_breaks(self):
|
||||||
|
""" Return the associated price breaks in the correct order """
|
||||||
|
return self.pricebreaks.order_by('quantity').all()
|
||||||
|
|
||||||
|
def get_price(self, quantity, moq=True, multiples=True):
|
||||||
|
""" Calculate the supplier price based on quantity price breaks.
|
||||||
|
|
||||||
|
- Don't forget to add in flat-fee cost (base_cost field)
|
||||||
|
- If MOQ (minimum order quantity) is required, bump quantity
|
||||||
|
- If order multiples are to be observed, then we need to calculate based on that, too
|
||||||
|
"""
|
||||||
|
|
||||||
|
price_breaks = self.price_breaks.all()
|
||||||
|
|
||||||
|
# No price break information available?
|
||||||
|
if len(price_breaks) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Minimum ordering requirement
|
||||||
|
if moq and self.minimum > quantity:
|
||||||
|
quantity = self.minimum
|
||||||
|
|
||||||
|
# Order multiples
|
||||||
|
if multiples:
|
||||||
|
quantity = int(math.ceil(quantity / self.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 rest_framework import serializers
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart, SupplierPriceBreak
|
||||||
|
|
||||||
|
from part.serializers import PartBriefSerializer
|
||||||
|
|
||||||
|
|
||||||
class CompanyBriefSerializer(serializers.ModelSerializer):
|
class CompanyBriefSerializer(serializers.ModelSerializer):
|
||||||
@ -47,3 +50,43 @@ class CompanySerializer(serializers.ModelSerializer):
|
|||||||
'is_supplier',
|
'is_supplier',
|
||||||
'part_count'
|
'part_count'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPartSerializer(serializers.ModelSerializer):
|
||||||
|
""" Serializer for SupplierPart object """
|
||||||
|
|
||||||
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
|
||||||
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
|
|
||||||
|
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
||||||
|
supplier_logo = serializers.CharField(source='supplier.get_image_url', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SupplierPart
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'url',
|
||||||
|
'part',
|
||||||
|
'part_detail',
|
||||||
|
'supplier',
|
||||||
|
'supplier_name',
|
||||||
|
'supplier_logo',
|
||||||
|
'SKU',
|
||||||
|
'manufacturer',
|
||||||
|
'MPN',
|
||||||
|
'URL',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPriceBreakSerializer(serializers.ModelSerializer):
|
||||||
|
""" Serializer for SupplierPriceBreak object """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SupplierPriceBreak
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'part',
|
||||||
|
'quantity',
|
||||||
|
'cost'
|
||||||
|
]
|
||||||
|
@ -10,7 +10,7 @@ InvenTree | {{ company.name }} - Parts
|
|||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<h3>Supplier Part</h3>
|
<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>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<h3>
|
<h3>
|
||||||
@ -59,29 +59,46 @@ InvenTree | {{ company.name }} - Parts
|
|||||||
|
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<table class="table table-striped table-condensed">
|
<table class="table table-striped table-condensed">
|
||||||
<tr><th colspan='2'>Pricing</th></tr>
|
<tr>
|
||||||
<tr><td>Single Price</td><td>{{ part.single_price }}</td></tr>
|
<th colspan='2'>Pricing</th>
|
||||||
{% if part.multiple > 1 %}
|
</tr>
|
||||||
<tr><td>Order Multiple</td><td>{{ part.multiple }}</td></tr>
|
<tr><td>Order Multiple</td><td>{{ part.multiple }}</td></tr>
|
||||||
{% endif %}
|
|
||||||
{% if part.base_cost > 0 %}
|
{% if part.base_cost > 0 %}
|
||||||
<tr><td>Base Price (Flat Fee)</td><td>{{ part.base_cost }}</td></tr>
|
<tr><td>Base Price (Flat Fee)</td><td>{{ part.base_cost }}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.minimum > 1 %}
|
{% if part.minimum > 1 %}
|
||||||
<tr><td>Minimum Order Quantity</td><td>{{ part.minimum }}</td></tr>
|
<tr><td>Minimum Order Quantity</td><td>{{ part.minimum }}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.price_breaks.all %}
|
<tr>
|
||||||
<tr><th colspan='2'>Price Breaks</th></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>
|
<tr>
|
||||||
<th>Quantity</th>
|
<th>Quantity</th>
|
||||||
<th>Price</th>
|
<th>Price</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if part.price_breaks.all %}
|
||||||
{% for pb in part.price_breaks.all %}
|
{% for pb in part.price_breaks.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ pb.quantity }}</td>
|
<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>
|
</tr>
|
||||||
{% endfor %}
|
{% 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 %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -89,9 +106,7 @@ InvenTree | {{ company.name }} - Parts
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class='btn btn-primary' type='button'>New Price Break</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include 'modals.html' %}
|
{% 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 %}
|
{% endblock %}
|
@ -36,3 +36,23 @@ company_urls = [
|
|||||||
# Redirect any other patterns
|
# Redirect any other patterns
|
||||||
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'),
|
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='company-index'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
price_break_urls = [
|
||||||
|
url('^new/', views.PriceBreakCreate.as_view(), name='price-break-create'),
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/edit/', views.PriceBreakEdit.as_view(), name='price-break-edit'),
|
||||||
|
url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
|
||||||
|
]
|
||||||
|
|
||||||
|
supplier_part_detail_urls = [
|
||||||
|
url(r'edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
|
||||||
|
url(r'delete/?', views.SupplierPartDelete.as_view(), name='supplier-part-delete'),
|
||||||
|
|
||||||
|
url('^.*$', views.SupplierPartDetail.as_view(), name='supplier-part-detail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
supplier_part_urls = [
|
||||||
|
url(r'^new/?', views.SupplierPartCreate.as_view(), name='supplier-part-create'),
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
|
||||||
|
]
|
||||||
|
@ -8,12 +8,20 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
|
from django.forms import HiddenInput
|
||||||
|
|
||||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import SupplierPart
|
||||||
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
|
from part.models import Part
|
||||||
|
|
||||||
from .forms import EditCompanyForm
|
from .forms import EditCompanyForm
|
||||||
from .forms import CompanyImageForm
|
from .forms import CompanyImageForm
|
||||||
|
from .forms import EditSupplierPartForm
|
||||||
|
from .forms import EditPriceBreakForm
|
||||||
|
|
||||||
|
|
||||||
class CompanyIndex(ListView):
|
class CompanyIndex(ListView):
|
||||||
@ -104,3 +112,142 @@ class CompanyDelete(AjaxDeleteView):
|
|||||||
return {
|
return {
|
||||||
'danger': 'Company was deleted',
|
'danger': 'Company was deleted',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPartDetail(DetailView):
|
||||||
|
""" Detail view for SupplierPart """
|
||||||
|
model = SupplierPart
|
||||||
|
template_name = 'company/partdetail.html'
|
||||||
|
context_object_name = 'part'
|
||||||
|
queryset = SupplierPart.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPartEdit(AjaxUpdateView):
|
||||||
|
""" Update view for editing SupplierPart """
|
||||||
|
|
||||||
|
model = SupplierPart
|
||||||
|
context_object_name = 'part'
|
||||||
|
form_class = EditSupplierPartForm
|
||||||
|
ajax_template_name = 'modal_form.html'
|
||||||
|
ajax_form_title = 'Edit Supplier Part'
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPartCreate(AjaxCreateView):
|
||||||
|
""" Create view for making new SupplierPart """
|
||||||
|
|
||||||
|
model = SupplierPart
|
||||||
|
form_class = EditSupplierPartForm
|
||||||
|
ajax_template_name = 'modal_form.html'
|
||||||
|
ajax_form_title = 'Create new Supplier Part'
|
||||||
|
context_object_name = 'part'
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
""" Create Form instance to create a new SupplierPart object.
|
||||||
|
Hide some fields if they are not appropriate in context
|
||||||
|
"""
|
||||||
|
form = super(AjaxCreateView, self).get_form()
|
||||||
|
|
||||||
|
if form.initial.get('supplier', None):
|
||||||
|
# Hide the supplier field
|
||||||
|
form.fields['supplier'].widget = HiddenInput()
|
||||||
|
|
||||||
|
if form.initial.get('part', None):
|
||||||
|
# Hide the part field
|
||||||
|
form.fields['part'].widget = HiddenInput()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
""" Provide initial data for new SupplierPart:
|
||||||
|
|
||||||
|
- If 'supplier_id' provided, pre-fill supplier field
|
||||||
|
- If 'part_id' provided, pre-fill part field
|
||||||
|
"""
|
||||||
|
initials = super(SupplierPartCreate, self).get_initial().copy()
|
||||||
|
|
||||||
|
supplier_id = self.get_param('supplier')
|
||||||
|
part_id = self.get_param('part')
|
||||||
|
|
||||||
|
if supplier_id:
|
||||||
|
try:
|
||||||
|
initials['supplier'] = Company.objects.get(pk=supplier_id)
|
||||||
|
except Company.DoesNotExist:
|
||||||
|
initials['supplier'] = None
|
||||||
|
|
||||||
|
if part_id:
|
||||||
|
try:
|
||||||
|
initials['part'] = Part.objects.get(pk=part_id)
|
||||||
|
except Part.DoesNotExist:
|
||||||
|
initials['part'] = None
|
||||||
|
|
||||||
|
return initials
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPartDelete(AjaxDeleteView):
|
||||||
|
""" Delete view for removing a SupplierPart """
|
||||||
|
model = SupplierPart
|
||||||
|
success_url = '/supplier/'
|
||||||
|
ajax_template_name = 'company/partdelete.html'
|
||||||
|
ajax_form_title = 'Delete Supplier Part'
|
||||||
|
context_object_name = 'supplier_part'
|
||||||
|
|
||||||
|
|
||||||
|
class PriceBreakCreate(AjaxCreateView):
|
||||||
|
""" View for creating a supplier price break """
|
||||||
|
|
||||||
|
model = SupplierPriceBreak
|
||||||
|
form_class = EditPriceBreakForm
|
||||||
|
ajax_form_title = 'Add Price Break'
|
||||||
|
ajax_template_name = 'modal_form.html'
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return {
|
||||||
|
'success': 'Added new price break'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_part(self):
|
||||||
|
try:
|
||||||
|
return SupplierPart.objects.get(id=self.request.GET.get('part'))
|
||||||
|
except SupplierPart.DoesNotExist:
|
||||||
|
return SupplierPart.objects.get(id=self.request.POST.get('part'))
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
|
||||||
|
form = super(AjaxCreateView, self).get_form()
|
||||||
|
form.fields['part'].widget = HiddenInput()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
|
||||||
|
initials = super(AjaxCreateView, self).get_initial()
|
||||||
|
|
||||||
|
print("GETTING INITIAL DAtA")
|
||||||
|
|
||||||
|
initials['part'] = self.get_part()
|
||||||
|
|
||||||
|
return initials
|
||||||
|
|
||||||
|
|
||||||
|
class PriceBreakEdit(AjaxUpdateView):
|
||||||
|
""" View for editing a supplier price break """
|
||||||
|
|
||||||
|
model = SupplierPriceBreak
|
||||||
|
form_class = EditPriceBreakForm
|
||||||
|
ajax_form_title = 'Edit Price Break'
|
||||||
|
ajax_template_name = 'modal_form.html'
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
|
||||||
|
form = super(AjaxUpdateView, self).get_form()
|
||||||
|
form.fields['part'].widget = HiddenInput()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
class PriceBreakDelete(AjaxDeleteView):
|
||||||
|
""" View for deleting a supplier price break """
|
||||||
|
|
||||||
|
model = SupplierPriceBreak
|
||||||
|
ajax_form_title = "Delete Price Break"
|
||||||
|
ajax_template_name = 'modal_delete_form.html'
|
||||||
|
@ -5,6 +5,5 @@ It includes models for:
|
|||||||
|
|
||||||
- PartCategory
|
- PartCategory
|
||||||
- Part
|
- Part
|
||||||
- SupplierPart
|
|
||||||
- BomItem
|
- BomItem
|
||||||
"""
|
"""
|
||||||
|
@ -3,7 +3,6 @@ from import_export.admin import ImportExportModelAdmin
|
|||||||
|
|
||||||
from .models import PartCategory, Part
|
from .models import PartCategory, Part
|
||||||
from .models import PartAttachment, PartStar
|
from .models import PartAttachment, PartStar
|
||||||
from .models import SupplierPart
|
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
|
|
||||||
|
|
||||||
@ -31,10 +30,6 @@ class BomItemAdmin(ImportExportModelAdmin):
|
|||||||
list_display = ('part', 'sub_part', 'quantity')
|
list_display = ('part', 'sub_part', 'quantity')
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartAdmin(ImportExportModelAdmin):
|
|
||||||
list_display = ('part', 'supplier', 'SKU')
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
class ParameterTemplateAdmin(admin.ModelAdmin):
|
class ParameterTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'units', 'format')
|
list_display = ('name', 'units', 'format')
|
||||||
@ -49,4 +44,3 @@ admin.site.register(PartCategory, PartCategoryAdmin)
|
|||||||
admin.site.register(PartAttachment, PartAttachmentAdmin)
|
admin.site.register(PartAttachment, PartAttachmentAdmin)
|
||||||
admin.site.register(PartStar, PartStarAdmin)
|
admin.site.register(PartStar, PartStarAdmin)
|
||||||
admin.site.register(BomItem, BomItemAdmin)
|
admin.site.register(BomItem, BomItemAdmin)
|
||||||
admin.site.register(SupplierPart, SupplierPartAdmin)
|
|
||||||
|
@ -16,10 +16,8 @@ from django.conf.urls import url, include
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .models import Part, PartCategory, BomItem, PartStar
|
from .models import Part, PartCategory, BomItem, PartStar
|
||||||
from .models import SupplierPart, SupplierPriceBreak
|
|
||||||
|
|
||||||
from .serializers import PartSerializer, BomItemSerializer
|
from .serializers import PartSerializer, BomItemSerializer
|
||||||
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
|
|
||||||
from .serializers import CategorySerializer
|
from .serializers import CategorySerializer
|
||||||
from .serializers import PartStarSerializer
|
from .serializers import PartStarSerializer
|
||||||
|
|
||||||
@ -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 = [
|
cat_api_urls = [
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
|
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'),
|
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 = [
|
part_star_api_urls = [
|
||||||
url(r'^(?P<pk>\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'),
|
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'),
|
url(r'^.*$', PartStarList.as_view(), name='api-part-star-list'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
part_api_urls = [
|
part_api_urls = [
|
||||||
url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),
|
url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),
|
||||||
|
|
||||||
url(r'^category/', include(cat_api_urls)),
|
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'^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'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'),
|
||||||
|
|
||||||
url(r'^.*$', PartList.as_view(), name='api-part-list'),
|
url(r'^.*$', PartList.as_view(), name='api-part-list'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
bom_api_urls = [
|
bom_api_urls = [
|
||||||
# BOM Item Detail
|
# BOM Item Detail
|
||||||
url('^(?P<pk>\d+)/', BomDetail.as_view(), name='api-bom-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 Part, PartCategory, PartAttachment
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import SupplierPart
|
|
||||||
|
|
||||||
|
|
||||||
class PartImageForm(HelperForm):
|
class PartImageForm(HelperForm):
|
||||||
@ -141,24 +140,17 @@ class EditBomItemForm(HelperForm):
|
|||||||
widgets = {'part': forms.HiddenInput()}
|
widgets = {'part': forms.HiddenInput()}
|
||||||
|
|
||||||
|
|
||||||
class EditSupplierPartForm(HelperForm):
|
class PartPriceForm(forms.Form):
|
||||||
""" Form for editing a SupplierPart object """
|
""" Simple form for viewing part pricing information """
|
||||||
|
|
||||||
|
quantity = forms.IntegerField(
|
||||||
|
required=True,
|
||||||
|
initial=1,
|
||||||
|
help_text='Input quantity for price calculation'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SupplierPart
|
model = Part
|
||||||
fields = [
|
fields = [
|
||||||
'part',
|
'quantity'
|
||||||
'supplier',
|
|
||||||
'SKU',
|
|
||||||
'description',
|
|
||||||
'manufacturer',
|
|
||||||
'MPN',
|
|
||||||
'URL',
|
|
||||||
'note',
|
|
||||||
'single_price',
|
|
||||||
'base_cost',
|
|
||||||
'multiple',
|
|
||||||
'minimum',
|
|
||||||
'packaging',
|
|
||||||
'lead_time'
|
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Generated by Django 2.2 on 2019-05-18 14:04
|
||||||
# Generated by Django 1.11.12 on 2018-04-22 11:53
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
import InvenTree.validators
|
||||||
|
from django.conf import settings
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -13,7 +13,7 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('company', '0001_initial'),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -21,7 +21,9 @@ class Migration(migrations.Migration):
|
|||||||
name='BomItem',
|
name='BomItem',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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={
|
options={
|
||||||
'verbose_name': 'BOM Item',
|
'verbose_name': 'BOM Item',
|
||||||
@ -31,18 +33,24 @@ class Migration(migrations.Migration):
|
|||||||
name='Part',
|
name='Part',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('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)),
|
('IPN', models.CharField(blank=True, help_text='Internal Part Number', max_length=100)),
|
||||||
('URL', models.URLField(blank=True, help_text='Link to extenal URL')),
|
('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)),
|
('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)])),
|
('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?')),
|
('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?')),
|
('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?')),
|
('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?')),
|
('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)),
|
('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={
|
options={
|
||||||
'verbose_name': 'Part',
|
'verbose_name': 'Part',
|
||||||
@ -53,8 +61,8 @@ class Migration(migrations.Migration):
|
|||||||
name='PartAttachment',
|
name='PartAttachment',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('attachment', models.FileField(help_text='Select file to attach', upload_to=part.models.attach_file)),
|
||||||
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='part.Part')),
|
('comment', models.CharField(help_text='File comment', max_length=100)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@ -63,7 +71,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('description', models.CharField(max_length=250)),
|
('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={
|
options={
|
||||||
'verbose_name': 'Part Category',
|
'verbose_name': 'Part Category',
|
||||||
@ -71,63 +79,11 @@ class Migration(migrations.Migration):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='SupplierPart',
|
name='PartStar',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_users', to='part.Part')),
|
||||||
('manufacturer', models.CharField(blank=True, help_text='Manufacturer', max_length=100)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_parts', to=settings.AUTH_USER_MODEL)),
|
||||||
('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')),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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 os
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
import tablib
|
import tablib
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -32,7 +30,8 @@ import hashlib
|
|||||||
from InvenTree import helpers
|
from InvenTree import helpers
|
||||||
from InvenTree import validators
|
from InvenTree import validators
|
||||||
from InvenTree.models import InvenTreeTree
|
from InvenTree.models import InvenTreeTree
|
||||||
from company.models import Company
|
|
||||||
|
from company.models import SupplierPart
|
||||||
|
|
||||||
|
|
||||||
class PartCategory(InvenTreeTree):
|
class PartCategory(InvenTreeTree):
|
||||||
@ -317,7 +316,7 @@ class Part(models.Model):
|
|||||||
# Default to None if there are multiple suppliers to choose from
|
# Default to None if there are multiple suppliers to choose from
|
||||||
return None
|
return None
|
||||||
|
|
||||||
default_supplier = models.ForeignKey('part.SupplierPart',
|
default_supplier = models.ForeignKey(SupplierPart,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
help_text='Default supplier part',
|
help_text='Default supplier part',
|
||||||
@ -562,6 +561,203 @@ class Part(models.Model):
|
|||||||
""" Return the number of supplier parts available for this part """
|
""" Return the number of supplier parts available for this part """
|
||||||
return self.supplier_parts.count()
|
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):
|
def deepCopy(self, other, **kwargs):
|
||||||
""" Duplicates non-field data from another part.
|
""" Duplicates non-field data from another part.
|
||||||
Does not alter the normal fields of this 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,
|
part = models.ForeignKey(Part, on_delete=models.CASCADE,
|
||||||
related_name='attachments')
|
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')
|
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
|
@property
|
||||||
def basename(self):
|
def basename(self):
|
||||||
@ -800,164 +996,7 @@ class BomItem(models.Model):
|
|||||||
|
|
||||||
return base_quantity + self.get_overage_quantity(base_quantity)
|
return base_quantity + self.get_overage_quantity(base_quantity)
|
||||||
|
|
||||||
|
|
||||||
class SupplierPart(models.Model):
|
|
||||||
""" Represents a unique part as provided by a Supplier
|
|
||||||
Each SupplierPart is identified by a MPN (Manufacturer Part Number)
|
|
||||||
Each SupplierPart is also linked to a Part object.
|
|
||||||
A Part may be available from multiple suppliers
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
part: Link to the master Part
|
|
||||||
supplier: Company that supplies this SupplierPart object
|
|
||||||
SKU: Stock keeping unit (supplier part number)
|
|
||||||
manufacturer: Manufacturer name
|
|
||||||
MPN: Manufacture part number
|
|
||||||
URL: Link to external website for this part
|
|
||||||
description: Descriptive notes field
|
|
||||||
note: Longer form note field
|
|
||||||
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
|
@property
|
||||||
def manufacturer_string(self):
|
def price_info(self):
|
||||||
|
""" Return the price for this item in the BOM """
|
||||||
items = []
|
return self.sub_part.get_price_info(self.quantity)
|
||||||
|
|
||||||
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)
|
|
||||||
|
@ -5,7 +5,7 @@ JSON serializers for Part app
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Part, PartStar
|
from .models import Part, PartStar
|
||||||
from .models import SupplierPart, SupplierPriceBreak
|
|
||||||
from .models import PartCategory
|
from .models import PartCategory
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
image_url = serializers.CharField(source='get_image_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:
|
class Meta:
|
||||||
model = Part
|
model = Part
|
||||||
@ -43,6 +44,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
|
|||||||
'full_name',
|
'full_name',
|
||||||
'description',
|
'description',
|
||||||
'available_stock',
|
'available_stock',
|
||||||
|
'single_price_info',
|
||||||
'image_url',
|
'image_url',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -106,6 +108,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
sub_part_detail = PartBriefSerializer(source='sub_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:
|
class Meta:
|
||||||
model = BomItem
|
model = BomItem
|
||||||
@ -116,46 +119,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
'sub_part',
|
'sub_part',
|
||||||
'sub_part_detail',
|
'sub_part_detail',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'price_info',
|
||||||
'overage',
|
'overage',
|
||||||
'note',
|
'note',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartSerializer(serializers.ModelSerializer):
|
|
||||||
""" Serializer for SupplierPart object """
|
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
|
||||||
|
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
|
||||||
|
|
||||||
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
|
||||||
supplier_logo = serializers.CharField(source='supplier.get_image_url', read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = SupplierPart
|
|
||||||
fields = [
|
|
||||||
'pk',
|
|
||||||
'url',
|
|
||||||
'part',
|
|
||||||
'part_detail',
|
|
||||||
'supplier',
|
|
||||||
'supplier_name',
|
|
||||||
'supplier_logo',
|
|
||||||
'SKU',
|
|
||||||
'manufacturer',
|
|
||||||
'MPN',
|
|
||||||
'URL',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierPriceBreakSerializer(serializers.ModelSerializer):
|
|
||||||
""" Serializer for SupplierPriceBreak object """
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = SupplierPriceBreak
|
|
||||||
fields = [
|
|
||||||
'pk',
|
|
||||||
'part',
|
|
||||||
'quantity',
|
|
||||||
'cost'
|
|
||||||
]
|
|
||||||
|
@ -11,6 +11,14 @@
|
|||||||
|
|
||||||
<h3>Bill of Materials</h3>
|
<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.bom_checked_date %}
|
||||||
{% if part.is_bom_valid %}
|
{% if part.is_bom_valid %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
|
@ -32,10 +32,13 @@
|
|||||||
<p><i>{{ part.description }}</i></p>
|
<p><i>{{ part.description }}</i></p>
|
||||||
<p>
|
<p>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
{% include "qr_button.html" %}
|
|
||||||
<button type='button' class='btn btn-default btn-glyph' id='toggle-starred' title='Star this part'>
|
<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 %}'/>
|
<span id='part-star-icon' class='starred-part glyphicon {% if starred %}glyphicon-star{% else %}glyphicon-star-empty{% endif %}'/>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
<table class='table table-condensed'>
|
<table class='table table-condensed'>
|
||||||
@ -82,6 +85,25 @@
|
|||||||
<td>{{ part.allocation_count }}</td>
|
<td>{{ part.allocation_count }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% 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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -122,6 +144,15 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#price-button").click(function() {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'part-pricing' part.id %}",
|
||||||
|
{
|
||||||
|
submit_text: 'Calculate',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$("#toggle-starred").click(function() {
|
$("#toggle-starred").click(function() {
|
||||||
toggleStar({
|
toggleStar({
|
||||||
part: {{ part.id }},
|
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
|
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 = [
|
part_attachment_urls = [
|
||||||
url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
|
url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
|
||||||
url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
|
url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
|
||||||
@ -37,6 +24,7 @@ part_detail_urls = [
|
|||||||
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
|
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
|
||||||
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
|
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
|
||||||
url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'),
|
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'^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'),
|
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.models import model_to_dict
|
||||||
from django.forms import HiddenInput, CheckboxInput
|
from django.forms import HiddenInput, CheckboxInput
|
||||||
|
|
||||||
from company.models import Company
|
|
||||||
from .models import PartCategory, Part, PartAttachment
|
from .models import PartCategory, Part, PartAttachment
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import SupplierPart
|
|
||||||
from .models import match_part_names
|
from .models import match_part_names
|
||||||
|
|
||||||
|
from company.models import SupplierPart
|
||||||
|
|
||||||
from . import forms as part_forms
|
from . import forms as part_forms
|
||||||
|
|
||||||
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
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):
|
class CategoryDetail(DetailView):
|
||||||
""" Detail view for PartCategory """
|
""" Detail view for PartCategory """
|
||||||
model = PartCategory
|
model = PartCategory
|
||||||
@ -731,81 +805,3 @@ class BomItemDelete(AjaxDeleteView):
|
|||||||
ajax_template_name = 'part/bom-delete.html'
|
ajax_template_name = 'part/bom-delete.html'
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_form_title = 'Confim BOM item deletion'
|
ajax_form_title = 'Confim BOM item deletion'
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartDetail(DetailView):
|
|
||||||
""" Detail view for SupplierPart """
|
|
||||||
model = SupplierPart
|
|
||||||
template_name = 'company/partdetail.html'
|
|
||||||
context_object_name = 'part'
|
|
||||||
queryset = SupplierPart.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartEdit(AjaxUpdateView):
|
|
||||||
""" Update view for editing SupplierPart """
|
|
||||||
|
|
||||||
model = SupplierPart
|
|
||||||
context_object_name = 'part'
|
|
||||||
form_class = part_forms.EditSupplierPartForm
|
|
||||||
ajax_template_name = 'modal_form.html'
|
|
||||||
ajax_form_title = 'Edit Supplier Part'
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartCreate(AjaxCreateView):
|
|
||||||
""" Create view for making new SupplierPart """
|
|
||||||
|
|
||||||
model = SupplierPart
|
|
||||||
form_class = part_forms.EditSupplierPartForm
|
|
||||||
ajax_template_name = 'modal_form.html'
|
|
||||||
ajax_form_title = 'Create new Supplier Part'
|
|
||||||
context_object_name = 'part'
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
""" Create Form instance to create a new SupplierPart object.
|
|
||||||
Hide some fields if they are not appropriate in context
|
|
||||||
"""
|
|
||||||
form = super(AjaxCreateView, self).get_form()
|
|
||||||
|
|
||||||
if form.initial.get('supplier', None):
|
|
||||||
# Hide the supplier field
|
|
||||||
form.fields['supplier'].widget = HiddenInput()
|
|
||||||
|
|
||||||
if form.initial.get('part', None):
|
|
||||||
# Hide the part field
|
|
||||||
form.fields['part'].widget = HiddenInput()
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
""" Provide initial data for new SupplierPart:
|
|
||||||
|
|
||||||
- If 'supplier_id' provided, pre-fill supplier field
|
|
||||||
- If 'part_id' provided, pre-fill part field
|
|
||||||
"""
|
|
||||||
initials = super(SupplierPartCreate, self).get_initial().copy()
|
|
||||||
|
|
||||||
supplier_id = self.get_param('supplier')
|
|
||||||
part_id = self.get_param('part')
|
|
||||||
|
|
||||||
if supplier_id:
|
|
||||||
try:
|
|
||||||
initials['supplier'] = Company.objects.get(pk=supplier_id)
|
|
||||||
except Company.DoesNotExist:
|
|
||||||
initials['supplier'] = None
|
|
||||||
|
|
||||||
if part_id:
|
|
||||||
try:
|
|
||||||
initials['part'] = Part.objects.get(pk=part_id)
|
|
||||||
except Part.DoesNotExist:
|
|
||||||
initials['part'] = None
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartDelete(AjaxDeleteView):
|
|
||||||
""" Delete view for removing a SupplierPart """
|
|
||||||
model = SupplierPart
|
|
||||||
success_url = '/supplier/'
|
|
||||||
ajax_template_name = 'company/partdelete.html'
|
|
||||||
ajax_form_title = 'Delete Supplier Part'
|
|
||||||
context_object_name = 'supplier_part'
|
|
||||||
|
@ -3,15 +3,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.glyphicon {
|
.glyphicon {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glyphicon-small {
|
.glyphicon-small {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.starred-part {
|
.starred-part {
|
||||||
color: #ffcc00;
|
color: #ffbb00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-price {
|
||||||
|
color: rgb(13, 245, 25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CSS overrides for treeview */
|
/* 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
|
// Part notes
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Generated by Django 2.2 on 2019-05-18 14:04
|
||||||
# Generated by Django 1.11.12 on 2018-04-22 11:53
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
@ -14,8 +12,8 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('part', '0001_initial'),
|
|
||||||
('company', '0001_initial'),
|
('company', '0001_initial'),
|
||||||
|
('part', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -25,30 +23,19 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('serial', models.PositiveIntegerField(blank=True, help_text='Serial number for this item', null=True)),
|
||||||
('URL', models.URLField(blank=True, max_length=125)),
|
('URL', models.URLField(blank=True, max_length=125)),
|
||||||
('batch', models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100)),
|
('batch', models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100, null=True)),
|
||||||
('quantity', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(0)])),
|
('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)])),
|
||||||
('updated', models.DateField(auto_now=True)),
|
('updated', models.DateField(auto_now=True, null=True)),
|
||||||
('stocktake_date', models.DateField(blank=True, null=True)),
|
('stocktake_date', models.DateField(blank=True, null=True)),
|
||||||
('review_needed', models.BooleanField(default=False)),
|
('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)])),
|
('delete_on_deplete', models.BooleanField(default=True, help_text='Delete this Stock Item when stock is depleted')),
|
||||||
('notes', models.TextField(blank=True)),
|
('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)),
|
('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')),
|
('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')),
|
('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(
|
migrations.CreateModel(
|
||||||
name='StockLocation',
|
name='StockLocation',
|
||||||
fields=[
|
fields=[
|
||||||
@ -59,34 +46,44 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'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(
|
migrations.AddField(
|
||||||
model_name='stockitem',
|
model_name='stockitem',
|
||||||
name='location',
|
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(
|
migrations.AddField(
|
||||||
model_name='stockitem',
|
model_name='stockitem',
|
||||||
name='part',
|
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(
|
migrations.AddField(
|
||||||
model_name='stockitem',
|
model_name='stockitem',
|
||||||
name='stocktake_user',
|
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(
|
migrations.AddField(
|
||||||
model_name='stockitem',
|
model_name='stockitem',
|
||||||
name='supplier_part',
|
name='supplier_part',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='part.SupplierPart'),
|
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='stocklocation',
|
|
||||||
unique_together=set([('name', 'parent')]),
|
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='stockitem',
|
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')
|
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')
|
help_text='Select a matching supplier part for this stock item')
|
||||||
|
|
||||||
location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,
|
location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,
|
||||||
|
Loading…
Reference in New Issue
Block a user