diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py
index ba3648da30..884bd0c49f 100644
--- a/InvenTree/InvenTree/fields.py
+++ b/InvenTree/InvenTree/fields.py
@@ -11,7 +11,7 @@ from django.core import validators
from django import forms
from decimal import Decimal
-from InvenTree.helpers import normalize
+import InvenTree.helpers
class InvenTreeURLFormField(FormURLField):
@@ -55,7 +55,7 @@ class RoundingDecimalFormField(forms.DecimalField):
"""
if type(value) == Decimal:
- return normalize(value)
+ return InvenTree.helpers.normalize(value)
else:
return value
diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 4ec84c7912..9b470902b1 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -15,7 +15,8 @@ from django.http import StreamingHttpResponse
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
-from .version import inventreeVersion, inventreeInstanceName
+import InvenTree.version
+
from .settings import MEDIA_URL, STATIC_URL
@@ -263,8 +264,8 @@ def MakeBarcode(object_name, object_pk, object_data, **kwargs):
data[object_name] = object_pk
else:
data['tool'] = 'InvenTree'
- data['version'] = inventreeVersion()
- data['instance'] = inventreeInstanceName()
+ data['version'] = InvenTree.version.inventreeVersion()
+ data['instance'] = InvenTree.version.inventreeInstanceName()
# Ensure PK is included
object_data['id'] = object_pk
diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
index b1e455283b..548bce12ab 100644
--- a/InvenTree/InvenTree/validators.py
+++ b/InvenTree/InvenTree/validators.py
@@ -6,7 +6,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
-from common.models import InvenTreeSetting
+import common.models
import re
@@ -43,7 +43,7 @@ def validate_part_name(value):
def validate_part_ipn(value):
""" Validate the Part IPN against regex rule """
- pattern = InvenTreeSetting.get_setting('part_ipn_regex')
+ pattern = common.models.InvenTreeSetting.get_setting('part_ipn_regex')
if pattern:
match = re.search(pattern, value)
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index e000f40076..b64a23a6fb 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -3,15 +3,16 @@ Provides information on the current InvenTree version
"""
import subprocess
-from common.models import InvenTreeSetting
import django
+import common.models
+
INVENTREE_SW_VERSION = "0.1.3 pre"
def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """
- return InvenTreeSetting.get_setting("InstanceName", "")
+ return common.models.InvenTreeSetting.get_setting("InstanceName", "")
def inventreeVersion():
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 89e79761e8..35870adde4 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -22,8 +22,8 @@ from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey
from InvenTree.status_codes import BuildStatus
-from InvenTree.fields import InvenTreeURLField
from InvenTree.helpers import decimal2string
+import InvenTree.fields
from stock import models as StockModels
from part import models as PartModels
@@ -151,7 +151,7 @@ class Build(MPTTModel):
related_name='builds_completed'
)
- link = InvenTreeURLField(
+ link = InvenTree.fields.InvenTreeURLField(
verbose_name=_('External Link'),
blank=True, help_text=_('Link to external URL')
)
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index b8e9959f01..b0a836118d 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -7,6 +7,7 @@ These models are 'generic' and do not fit a particular business logic object.
from __future__ import unicode_literals
import os
+import decimal
from django.db import models
from django.conf import settings
@@ -14,6 +15,8 @@ from django.utils.translation import ugettext as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError
+import InvenTree.fields
+
class InvenTreeSetting(models.Model):
"""
@@ -159,6 +162,42 @@ class Currency(models.Model):
super().save(*args, **kwargs)
+class PriceBreak(models.Model):
+ """
+ Represents a PriceBreak model
+ """
+
+ class Meta:
+ abstract = True
+
+ quantity = InvenTree.fields.RoundingDecimalField(max_digits=15, decimal_places=5, default=1, validators=[MinValueValidator(1)])
+
+ cost = InvenTree.fields.RoundingDecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)])
+
+ currency = models.ForeignKey(Currency, blank=True, null=True, on_delete=models.SET_NULL)
+
+ @property
+ def symbol(self):
+ return self.currency.symbol if self.currency else ''
+
+ @property
+ def suffix(self):
+ return self.currency.suffix if self.currency else ''
+
+ @property
+ def converted_cost(self):
+ """
+ Return the cost of this price break, converted to the base currency
+ """
+
+ scaler = decimal.Decimal(1.0)
+
+ if self.currency:
+ scaler = self.currency.value
+
+ return self.cost * scaler
+
+
class ColorTheme(models.Model):
""" Color Theme Setting """
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index b938266bd5..2a8d907b76 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -8,7 +8,6 @@ from __future__ import unicode_literals
import os
import math
-from decimal import Decimal
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator
@@ -24,9 +23,10 @@ from stdimage.models import StdImageField
from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail
from InvenTree.helpers import normalize
-from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
+from InvenTree.fields import InvenTreeURLField
from InvenTree.status_codes import PurchaseOrderStatus
-from common.models import Currency
+
+import common.models
def rename_company_image(instance, filename):
@@ -433,7 +433,7 @@ class SupplierPart(models.Model):
return s
-class SupplierPriceBreak(models.Model):
+class SupplierPriceBreak(common.models.PriceBreak):
""" 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)
@@ -447,23 +447,6 @@ class SupplierPriceBreak(models.Model):
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks')
- quantity = RoundingDecimalField(max_digits=15, decimal_places=5, default=1, validators=[MinValueValidator(1)])
-
- cost = RoundingDecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)])
-
- currency = models.ForeignKey(Currency, blank=True, null=True, on_delete=models.SET_NULL)
-
- @property
- def converted_cost(self):
- """ Return the cost of this price break, converted to the base currency """
-
- scaler = Decimal(1.0)
-
- if self.currency:
- scaler = self.currency.value
-
- return self.cost * scaler
-
class Meta:
unique_together = ("part", "quantity")
diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py
index a80b3b55a9..f6de7d4f50 100644
--- a/InvenTree/company/serializers.py
+++ b/InvenTree/company/serializers.py
@@ -137,11 +137,22 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPriceBreak object """
+ symbol = serializers.CharField(read_only=True)
+
+ suffix = serializers.CharField(read_only=True)
+
+ quantity = serializers.FloatField()
+
+ cost = serializers.FloatField()
+
class Meta:
model = SupplierPriceBreak
fields = [
'pk',
'part',
'quantity',
- 'cost'
+ 'cost',
+ 'currency',
+ 'symbol',
+ 'suffix',
]
diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html
index 080871ace7..28cc917e1a 100644
--- a/InvenTree/company/templates/company/supplier_part_pricing.html
+++ b/InvenTree/company/templates/company/supplier_part_pricing.html
@@ -10,45 +10,12 @@
{% trans "Pricing Information" %}
-
- {% trans "Order Multiple" %} | {{ part.multiple }} |
- {% if part.base_cost > 0 %}
- {% trans "Base Price (Flat Fee)" %} | {{ part.base_cost }} |
- {% endif %}
-
- {% trans "Price Breaks" %} |
-
-
-
-
- |
-
-
- {% trans "Quantity" %} |
- {% trans "Price" %} |
-
- {% if part.price_breaks.all %}
- {% for pb in part.price_breaks.all %}
-
- {% decimal pb.quantity %} |
-
- {% if pb.currency %}{{ pb.currency.symbol }}{% endif %}
- {% decimal pb.cost %}
- {% if pb.currency %}{{ pb.currency.suffix }}{% endif %}
-
-
-
-
- |
-
- {% endfor %}
- {% else %}
-
-
- {% trans "No price breaks have been added for this part" %}
- |
-
- {% endif %}
+
+
+
+
+
+
{% endblock %}
@@ -56,7 +23,80 @@
{% block js_ready %}
{{ block.super }}
+function reloadPriceBreaks() {
+ $("#price-break-table").bootstrapTable("refresh");
+}
+$('#price-break-table').inventreeTable({
+ name: 'buypricebreaks',
+ formatNoMatches: function() { return "{% trans "No price break information found" %}"; },
+ queryParams: {
+ part: {{ part.id }},
+ },
+ url: "{% url 'api-part-supplier-price' %}",
+ onLoadSuccess: function() {
+ var table = $('#price-break-table');
+
+ table.find('.button-price-break-delete').click(function() {
+ var pk = $(this).attr('pk');
+
+ launchModalForm(
+ `/price-break/${pk}/delete/`,
+ {
+ success: reloadPriceBreaks
+ }
+ );
+ });
+
+ table.find('.button-price-break-edit').click(function() {
+ var pk = $(this).attr('pk');
+
+ launchModalForm(
+ `/price-break/${pk}/edit/`,
+ {
+ success: reloadPriceBreaks
+ }
+ );
+ });
+ },
+ columns: [
+ {
+ field: 'pk',
+ title: 'ID',
+ visible: false,
+ switchable: false,
+ },
+ {
+ field: 'quantity',
+ title: '{% trans "Quantity" %}',
+ sortable: true,
+ },
+ {
+ field: 'cost',
+ title: '{% trans "Price" %}',
+ sortable: true,
+ formatter: function(value, row, index) {
+ var html = '';
+
+ html += row.symbol || '';
+ html += value;
+
+ if (row.suffix) {
+ html += ' ' + row.suffix || '';
+ }
+
+ html += ``
+
+ html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}');
+ html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
+
+ html += `
`;
+
+ return html;
+ }
+ },
+ ]
+});
$('#new-price-break').click(function() {
launchModalForm("{% url 'price-break-create' %}",
@@ -69,24 +109,4 @@ $('#new-price-break').click(function() {
);
});
-$('.pb-edit-button').click(function() {
- var button = $(this);
-
- launchModalForm(button.attr('url'),
- {
- reload: true,
- }
- );
-});
-
-$('.pb-delete-button').click(function() {
- var button = $(this);
-
- launchModalForm(button.attr('url'),
- {
- reload: true,
- }
- );
-});
-
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py
index 457cf74ec2..9ef6adea0e 100644
--- a/InvenTree/company/views.py
+++ b/InvenTree/company/views.py
@@ -401,7 +401,7 @@ class PriceBreakCreate(AjaxCreateView):
def get_data(self):
return {
- 'success': 'Added new price break'
+ 'success': _('Added new price break')
}
def get_part(self):
diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py
index 568a48034f..fce80eb219 100644
--- a/InvenTree/part/admin.py
+++ b/InvenTree/part/admin.py
@@ -13,6 +13,7 @@ from .models import PartAttachment, PartStar
from .models import BomItem
from .models import PartParameterTemplate, PartParameter
from .models import PartTestTemplate
+from .models import PartSellPriceBreak
from stock.models import StockLocation
from company.models import SupplierPart
@@ -257,6 +258,14 @@ class ParameterAdmin(ImportExportModelAdmin):
list_display = ('part', 'template', 'data')
+class PartSellPriceBreakAdmin(admin.ModelAdmin):
+
+ class Meta:
+ model = PartSellPriceBreak
+
+ list_display = ('part', 'quantity', 'cost', 'currency')
+
+
admin.site.register(Part, PartAdmin)
admin.site.register(PartCategory, PartCategoryAdmin)
admin.site.register(PartAttachment, PartAttachmentAdmin)
@@ -265,3 +274,4 @@ admin.site.register(BomItem, BomItemAdmin)
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
admin.site.register(PartParameter, ParameterAdmin)
admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
+admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 9d9d56d253..72b72f0dae 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -20,6 +20,7 @@ from django.urls import reverse
from .models import Part, PartCategory, BomItem, PartStar
from .models import PartParameter, PartParameterTemplate
from .models import PartAttachment, PartTestTemplate
+from .models import PartSellPriceBreak
from . import serializers as part_serializers
@@ -107,6 +108,27 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = PartCategory.objects.all()
+class PartSalePriceList(generics.ListCreateAPIView):
+ """
+ API endpoint for list view of PartSalePriceBreak model
+ """
+
+ queryset = PartSellPriceBreak.objects.all()
+ serializer_class = part_serializers.PartSalePriceSerializer
+
+ permission_classes = [
+ permissions.IsAuthenticated,
+ ]
+
+ filter_backends = [
+ DjangoFilterBackend
+ ]
+
+ filter_fields = [
+ 'part',
+ ]
+
+
class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a PartAttachment (file upload).
@@ -809,6 +831,11 @@ part_api_urls = [
url(r'^(?P\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'),
url(r'^$', PartStarList.as_view(), name='api-part-star-list'),
])),
+
+ # Base URL for part sale pricing
+ url(r'^sale-price/', include([
+ url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
+ ])),
# Base URL for PartParameter API endpoints
url(r'^parameter/', include([
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index ffcb46114a..0a15d598bd 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -17,6 +17,8 @@ from .models import Part, PartCategory, PartAttachment
from .models import BomItem
from .models import PartParameterTemplate, PartParameter
from .models import PartTestTemplate
+from .models import PartSellPriceBreak
+
from common.models import Currency
@@ -253,3 +255,22 @@ class PartPriceForm(forms.Form):
'quantity',
'currency',
]
+
+
+class EditPartSalePriceBreakForm(HelperForm):
+ """
+ Form for creating / editing a sale price for a part
+ """
+
+ quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
+
+ cost = RoundingDecimalFormField(max_digits=10, decimal_places=5)
+
+ class Meta:
+ model = PartSellPriceBreak
+ fields = [
+ 'part',
+ 'quantity',
+ 'cost',
+ 'currency',
+ ]
diff --git a/InvenTree/part/migrations/0049_partsellpricebreak.py b/InvenTree/part/migrations/0049_partsellpricebreak.py
new file mode 100644
index 0000000000..1d49dcbfac
--- /dev/null
+++ b/InvenTree/part/migrations/0049_partsellpricebreak.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.0.7 on 2020-09-17 13:22
+
+import InvenTree.fields
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('common', '0007_colortheme'),
+ ('part', '0048_auto_20200902_1404'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PartSellPriceBreak',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(1)])),
+ ('cost', InvenTree.fields.RoundingDecimalField(decimal_places=5, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
+ ('currency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Currency')),
+ ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='salepricebreaks', to='part.Part')),
+ ],
+ options={
+ 'unique_together': {('part', 'quantity')},
+ },
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0050_auto_20200917_2315.py b/InvenTree/part/migrations/0050_auto_20200917_2315.py
new file mode 100644
index 0000000000..635294093d
--- /dev/null
+++ b/InvenTree/part/migrations/0050_auto_20200917_2315.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.7 on 2020-09-17 23:15
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0049_partsellpricebreak'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='partsellpricebreak',
+ name='part',
+ field=models.ForeignKey(limit_choices_to={'salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='salepricebreaks', to='part.Part'),
+ ),
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index dc9c3102f0..afb2dfa64e 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -46,6 +46,8 @@ from order import models as OrderModels
from company.models import SupplierPart
from stock import models as StockModels
+import common.models
+
class PartCategory(InvenTreeTree):
""" PartCategory provides hierarchical organization of Part objects.
@@ -1226,6 +1228,21 @@ class PartAttachment(InvenTreeAttachment):
related_name='attachments')
+class PartSellPriceBreak(common.models.PriceBreak):
+ """
+ Represents a price break for selling this part
+ """
+
+ part = models.ForeignKey(
+ Part, on_delete=models.CASCADE,
+ related_name='salepricebreaks',
+ limit_choices_to={'salable': True}
+ )
+
+ class Meta:
+ unique_together = ('part', 'quantity')
+
+
class PartStar(models.Model):
""" A PartStar object creates a relationship between a User and a Part.
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 8adb26680e..7c73e9f98b 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -12,6 +12,7 @@ from .models import BomItem
from .models import PartParameter, PartParameterTemplate
from .models import PartAttachment
from .models import PartTestTemplate
+from .models import PartSellPriceBreak
from stock.models import StockItem
@@ -87,6 +88,32 @@ class PartTestTemplateSerializer(InvenTreeModelSerializer):
]
+class PartSalePriceSerializer(InvenTreeModelSerializer):
+ """
+ Serializer for sale prices for Part model.
+ """
+
+ symbol = serializers.CharField(read_only=True)
+
+ suffix = serializers.CharField(read_only=True)
+
+ quantity = serializers.FloatField()
+
+ cost = serializers.FloatField()
+
+ class Meta:
+ model = PartSellPriceBreak
+ fields = [
+ 'pk',
+ 'part',
+ 'quantity',
+ 'cost',
+ 'currency',
+ 'symbol',
+ 'suffix',
+ ]
+
+
class PartThumbSerializer(serializers.Serializer):
"""
Serializer for the 'image' field of the Part model.
diff --git a/InvenTree/part/templates/part/sale_prices.html b/InvenTree/part/templates/part/sale_prices.html
new file mode 100644
index 0000000000..8d3cc61afd
--- /dev/null
+++ b/InvenTree/part/templates/part/sale_prices.html
@@ -0,0 +1,110 @@
+{% extends "part/part_base.html" %}
+{% load static %}
+{% load i18n %}
+
+{% block details %}
+
+{% include 'part/tabs.html' with tab='sales-prices' %}
+
+{% trans "Sale Price" %}
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block js_ready %}
+{{ block.super }}
+
+function reloadPriceBreaks() {
+ $("#price-break-table").bootstrapTable("refresh");
+}
+
+$('#new-price-break').click(function() {
+ launchModalForm("{% url 'sale-price-break-create' %}",
+ {
+ success: reloadPriceBreaks,
+ data: {
+ part: {{ part.id }},
+ }
+ }
+ );
+});
+
+$('#price-break-table').inventreeTable({
+ name: 'saleprice',
+ formatNoMatches: function() { return "{% trans 'No price break information found' %}"; },
+ queryParams: {
+ part: {{ part.id }},
+ },
+ url: "{% url 'api-part-sale-price-list' %}",
+ onLoadSuccess: function() {
+ var table = $('#price-break-table');
+
+ table.find('.button-price-break-delete').click(function() {
+ var pk = $(this).attr('pk');
+
+ launchModalForm(
+ `/part/sale-price/${pk}/delete/`,
+ {
+ success: reloadPriceBreaks
+ }
+ );
+ });
+
+ table.find('.button-price-break-edit').click(function() {
+ var pk = $(this).attr('pk');
+
+ launchModalForm(
+ `/part/sale-price/${pk}/edit/`,
+ {
+ success: reloadPriceBreaks
+ }
+ );
+ });
+ },
+ columns: [
+ {
+ field: 'pk',
+ title: 'ID',
+ visible: false,
+ switchable: false,
+ },
+ {
+ field: 'quantity',
+ title: '{% trans "Quantity" %}',
+ sortable: true,
+ },
+ {
+ field: 'cost',
+ title: '{% trans "Price" %}',
+ sortable: true,
+ formatter: function(value, row, index) {
+ var html = '';
+
+ html += row.symbol || '';
+ html += value;
+
+ if (row.suffix) {
+ html += ' ' + row.suffix || '';
+ }
+
+ html += ``
+
+ html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}');
+ html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
+
+ html += `
`;
+
+ return html;
+ }
+ },
+ ]
+})
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html
index ffb92cf191..1eab299ed5 100644
--- a/InvenTree/part/templates/part/tabs.html
+++ b/InvenTree/part/templates/part/tabs.html
@@ -46,6 +46,9 @@
{% endif %}
{% if part.salable %}
+
+ {% trans "Sale Price" %}
+
{% trans "Sales Orders" %} {{ part.sales_orders|length }}
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index 2707a563c7..e61947e243 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -18,6 +18,12 @@ part_attachment_urls = [
url(r'^(?P\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'),
]
+sale_price_break_urls = [
+ url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
+ url(r'^(?P\d+)/edit/', views.PartSalePriceBreakEdit.as_view(), name='sale-price-break-edit'),
+ url(r'^(?P\d+)/delete/', views.PartSalePriceBreakDelete.as_view(), name='sale-price-break-delete'),
+]
+
part_parameter_urls = [
url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'),
@@ -27,7 +33,6 @@ part_parameter_urls = [
url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'),
url(r'^(?P\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
url(r'^(?P\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'),
-
]
part_detail_urls = [
@@ -52,6 +57,7 @@ part_detail_urls = [
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'),
url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'),
+ url(r'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'),
url(r'^tests/', views.PartDetail.as_view(template_name='part/part_tests.html'), name='part-test-templates'),
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'),
@@ -108,6 +114,9 @@ part_urls = [
# Part attachments
url(r'^attachment/', include(part_attachment_urls)),
+ # Part price breaks
+ url(r'^sale-price/', include(sale_price_break_urls)),
+
# Part test templates
url(r'^test-template/', include([
url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 58ece9d0b0..ccf607afc0 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -26,6 +26,7 @@ from .models import PartParameterTemplate, PartParameter
from .models import BomItem
from .models import match_part_names
from .models import PartTestTemplate
+from .models import PartSellPriceBreak
from common.models import Currency, InvenTreeSetting
from company.models import SupplierPart
@@ -2097,3 +2098,75 @@ class BomItemDelete(AjaxDeleteView):
ajax_template_name = 'part/bom-delete.html'
context_object_name = 'item'
ajax_form_title = _('Confim BOM item deletion')
+
+
+class PartSalePriceBreakCreate(AjaxCreateView):
+ """ View for creating a sale price break for a part """
+
+ model = PartSellPriceBreak
+ form_class = part_forms.EditPartSalePriceBreakForm
+ ajax_form_title = _('Add Price Break')
+
+ def get_data(self):
+ return {
+ 'success': _('Added new price break')
+ }
+
+ def get_part(self):
+ try:
+ part = Part.objects.get(id=self.request.GET.get('part'))
+ except (ValueError, Part.DoesNotExist):
+ part = None
+
+ if part is None:
+ try:
+ part = Part.objects.get(id=self.request.POST.get('part'))
+ except (ValueError, Part.DoesNotExist):
+ part = None
+
+ return 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()
+
+ initials['part'] = self.get_part()
+
+ # Pre-select the default currency
+ try:
+ base = Currency.objects.get(base=True)
+ initials['currency'] = base
+ except Currency.DoesNotExist:
+ pass
+
+ return initials
+
+
+class PartSalePriceBreakEdit(AjaxUpdateView):
+ """ View for editing a sale price break """
+
+ model = PartSellPriceBreak
+ form_class = part_forms.EditPartSalePriceBreakForm
+ ajax_form_title = _('Edit Price Break')
+
+ def get_form(self):
+
+ form = super().get_form()
+ form.fields['part'].widget = HiddenInput()
+
+ return form
+
+
+class PartSalePriceBreakDelete(AjaxDeleteView):
+ """ View for deleting a sale price break """
+
+ model = PartSellPriceBreak
+ ajax_form_title = _("Delete Price Break")
+ ajax_template_name = "modal_delete_form.html"
diff --git a/tasks.py b/tasks.py
index 51c5a68849..cbf9d1722c 100644
--- a/tasks.py
+++ b/tasks.py
@@ -108,6 +108,14 @@ def superuser(c):
manage(c, 'createsuperuser', pty=True)
+@task
+def check(c):
+ """
+ Check validity of django codebase
+ """
+
+ manage(c, "check")
+
@task
def migrate(c):
"""