Merge pull request #988 from SchrodingersGat/sell-price

Sell price
This commit is contained in:
Oliver 2020-09-19 23:52:25 +10:00 committed by GitHub
commit 7dbb6c7c8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 503 additions and 94 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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():

View File

@ -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')
)

View File

@ -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 """

View File

@ -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")

View File

@ -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',
]

View File

@ -10,45 +10,12 @@
<hr>
<h4>{% trans "Pricing Information" %}</h4>
<table class="table table-striped table-condensed">
<tr><td>{% trans "Order Multiple" %}</td><td>{{ part.multiple }}</td></tr>
{% if part.base_cost > 0 %}
<tr><td>{% trans "Base Price (Flat Fee)" %}</td><td>{{ part.base_cost }}</td></tr>
{% endif %}
<tr>
<th>{% trans "Price Breaks" %}</th>
<th>
<div style='float: right;'>
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "New Price Break" %}</button>
</div>
</th>
</tr>
<tr>
<th>{% trans "Quantity" %}</th>
<th>{% trans "Price" %}</th>
</tr>
{% if part.price_breaks.all %}
{% for pb in part.price_breaks.all %}
<tr>
<td>{% decimal pb.quantity %}</td>
<td>
{% if pb.currency %}{{ pb.currency.symbol }}{% endif %}
{% decimal pb.cost %}
{% if pb.currency %}{{ pb.currency.suffix }}{% endif %}
<div class='btn-group' style='float: right;'>
<button title='Edit Price Break' class='btn btn-default btn-sm pb-edit-button' type='button' url="{% url 'price-break-edit' pb.id %}"><span class='fas fa-edit icon-green'></span></button>
<button title='Delete Price Break' class='btn btn-default btn-sm pb-delete-button' type='button' url="{% url 'price-break-delete' pb.id %}"><span class='fas fa-trash-alt icon-red'></span></button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan='2'>
<span class='warning-msg'><i>{% trans "No price breaks have been added for this part" %}</i></span>
</td>
</tr>
{% endif %}
<div id='price-break-toolbar' class='btn-group'>
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button>
</div>
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
</table>
{% 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 += `<div class='btn-group float-right' role='group'>`
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 += `</div>`;
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 %}

View File

@ -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):

View File

@ -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)

View File

@ -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).
@ -810,6 +832,11 @@ part_api_urls = [
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([
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),

View File

@ -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',
]

View File

@ -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')},
},
),
]

View File

@ -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'),
),
]

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,110 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include 'part/tabs.html' with tab='sales-prices' %}
<h4>{% trans "Sale Price" %}</h4>
<hr>
<div id='price-break-toolbar' class='btn-group'>
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button>
</div>
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
</table>
{% 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 += `<div class='btn-group float-right' role='group'>`
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 += `</div>`;
return html;
}
},
]
})
{% endblock %}

View File

@ -46,6 +46,9 @@
</li>
{% endif %}
{% if part.salable %}
<li {% if tab == 'sales-prices' %}class='active'{% endif %}>
<a href="{% url 'part-sale-prices' part.id %}">{% trans "Sale Price" %}</a>
</li>
<li{% ifequal tab 'sales-orders' %} class='active'{% endifequal %}>
<a href="{% url 'part-sales-orders' part.id %}">{% trans "Sales Orders" %} <span class='badge'>{{ part.sales_orders|length }}</span></a>
</li>

View File

@ -18,6 +18,12 @@ part_attachment_urls = [
url(r'^(?P<pk>\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<pk>\d+)/edit/', views.PartSalePriceBreakEdit.as_view(), name='sale-price-break-edit'),
url(r'^(?P<pk>\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<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
url(r'^(?P<pk>\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'),

View File

@ -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"

View File

@ -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):
"""