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 django import forms
from decimal import Decimal from decimal import Decimal
from InvenTree.helpers import normalize import InvenTree.helpers
class InvenTreeURLFormField(FormURLField): class InvenTreeURLFormField(FormURLField):
@ -55,7 +55,7 @@ class RoundingDecimalFormField(forms.DecimalField):
""" """
if type(value) == Decimal: if type(value) == Decimal:
return normalize(value) return InvenTree.helpers.normalize(value)
else: else:
return value return value

View File

@ -15,7 +15,8 @@ from django.http import StreamingHttpResponse
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from .version import inventreeVersion, inventreeInstanceName import InvenTree.version
from .settings import MEDIA_URL, STATIC_URL 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 data[object_name] = object_pk
else: else:
data['tool'] = 'InvenTree' data['tool'] = 'InvenTree'
data['version'] = inventreeVersion() data['version'] = InvenTree.version.inventreeVersion()
data['instance'] = inventreeInstanceName() data['instance'] = InvenTree.version.inventreeInstanceName()
# Ensure PK is included # Ensure PK is included
object_data['id'] = object_pk object_data['id'] = object_pk

View File

@ -6,7 +6,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.models import InvenTreeSetting import common.models
import re import re
@ -43,7 +43,7 @@ def validate_part_name(value):
def validate_part_ipn(value): def validate_part_ipn(value):
""" Validate the Part IPN against regex rule """ """ 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: if pattern:
match = re.search(pattern, value) match = re.search(pattern, value)

View File

@ -3,15 +3,16 @@ Provides information on the current InvenTree version
""" """
import subprocess import subprocess
from common.models import InvenTreeSetting
import django import django
import common.models
INVENTREE_SW_VERSION = "0.1.3 pre" INVENTREE_SW_VERSION = "0.1.3 pre"
def inventreeInstanceName(): def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """ """ Returns the InstanceName settings for the current database """
return InvenTreeSetting.get_setting("InstanceName", "") return common.models.InvenTreeSetting.get_setting("InstanceName", "")
def inventreeVersion(): def inventreeVersion():

View File

@ -22,8 +22,8 @@ from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatus
from InvenTree.fields import InvenTreeURLField
from InvenTree.helpers import decimal2string from InvenTree.helpers import decimal2string
import InvenTree.fields
from stock import models as StockModels from stock import models as StockModels
from part import models as PartModels from part import models as PartModels
@ -151,7 +151,7 @@ class Build(MPTTModel):
related_name='builds_completed' related_name='builds_completed'
) )
link = InvenTreeURLField( link = InvenTree.fields.InvenTreeURLField(
verbose_name=_('External Link'), verbose_name=_('External Link'),
blank=True, help_text=_('Link to external URL') 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 from __future__ import unicode_literals
import os import os
import decimal
from django.db import models from django.db import models
from django.conf import settings 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.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import InvenTree.fields
class InvenTreeSetting(models.Model): class InvenTreeSetting(models.Model):
""" """
@ -159,6 +162,42 @@ class Currency(models.Model):
super().save(*args, **kwargs) 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): class ColorTheme(models.Model):
""" Color Theme Setting """ """ Color Theme Setting """

View File

@ -8,7 +8,6 @@ from __future__ import unicode_literals
import os import os
import math import math
from decimal import Decimal
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator 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 getMediaUrl, getBlankImage, getBlankThumbnail
from InvenTree.helpers import normalize from InvenTree.helpers import normalize
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField from InvenTree.fields import InvenTreeURLField
from InvenTree.status_codes import PurchaseOrderStatus from InvenTree.status_codes import PurchaseOrderStatus
from common.models import Currency
import common.models
def rename_company_image(instance, filename): def rename_company_image(instance, filename):
@ -433,7 +433,7 @@ class SupplierPart(models.Model):
return s return s
class SupplierPriceBreak(models.Model): class SupplierPriceBreak(common.models.PriceBreak):
""" Represents a quantity price break for a SupplierPart. """ Represents a quantity price break for a SupplierPart.
- Suppliers can offer discounts at larger quantities - Suppliers can offer discounts at larger quantities
- SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s) - 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') 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: class Meta:
unique_together = ("part", "quantity") unique_together = ("part", "quantity")

View File

@ -137,11 +137,22 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
class SupplierPriceBreakSerializer(InvenTreeModelSerializer): class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPriceBreak object """ """ Serializer for SupplierPriceBreak object """
symbol = serializers.CharField(read_only=True)
suffix = serializers.CharField(read_only=True)
quantity = serializers.FloatField()
cost = serializers.FloatField()
class Meta: class Meta:
model = SupplierPriceBreak model = SupplierPriceBreak
fields = [ fields = [
'pk', 'pk',
'part', 'part',
'quantity', 'quantity',
'cost' 'cost',
'currency',
'symbol',
'suffix',
] ]

View File

@ -10,45 +10,12 @@
<hr> <hr>
<h4>{% trans "Pricing Information" %}</h4> <h4>{% trans "Pricing Information" %}</h4>
<table class="table table-striped table-condensed">
<tr><td>{% trans "Order Multiple" %}</td><td>{{ part.multiple }}</td></tr> <div id='price-break-toolbar' class='btn-group'>
{% if part.base_cost > 0 %} <button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button>
<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> </div>
</th>
</tr> <table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
<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 %}
</table> </table>
{% endblock %} {% endblock %}
@ -56,7 +23,80 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ 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() { $('#new-price-break').click(function() {
launchModalForm("{% url 'price-break-create' %}", 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 %} {% endblock %}

View File

@ -401,7 +401,7 @@ class PriceBreakCreate(AjaxCreateView):
def get_data(self): def get_data(self):
return { return {
'success': 'Added new price break' 'success': _('Added new price break')
} }
def get_part(self): def get_part(self):

View File

@ -13,6 +13,7 @@ from .models import PartAttachment, PartStar
from .models import BomItem from .models import BomItem
from .models import PartParameterTemplate, PartParameter from .models import PartParameterTemplate, PartParameter
from .models import PartTestTemplate from .models import PartTestTemplate
from .models import PartSellPriceBreak
from stock.models import StockLocation from stock.models import StockLocation
from company.models import SupplierPart from company.models import SupplierPart
@ -257,6 +258,14 @@ class ParameterAdmin(ImportExportModelAdmin):
list_display = ('part', 'template', 'data') 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(Part, PartAdmin)
admin.site.register(PartCategory, PartCategoryAdmin) admin.site.register(PartCategory, PartCategoryAdmin)
admin.site.register(PartAttachment, PartAttachmentAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin)
@ -265,3 +274,4 @@ admin.site.register(BomItem, BomItemAdmin)
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
admin.site.register(PartParameter, ParameterAdmin) admin.site.register(PartParameter, ParameterAdmin)
admin.site.register(PartTestTemplate, PartTestTemplateAdmin) 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 Part, PartCategory, BomItem, PartStar
from .models import PartParameter, PartParameterTemplate from .models import PartParameter, PartParameterTemplate
from .models import PartAttachment, PartTestTemplate from .models import PartAttachment, PartTestTemplate
from .models import PartSellPriceBreak
from . import serializers as part_serializers from . import serializers as part_serializers
@ -107,6 +108,27 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = PartCategory.objects.all() 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): class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a PartAttachment (file upload). 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'), 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 # Base URL for PartParameter API endpoints
url(r'^parameter/', include([ url(r'^parameter/', include([
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'), 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 BomItem
from .models import PartParameterTemplate, PartParameter from .models import PartParameterTemplate, PartParameter
from .models import PartTestTemplate from .models import PartTestTemplate
from .models import PartSellPriceBreak
from common.models import Currency from common.models import Currency
@ -253,3 +255,22 @@ class PartPriceForm(forms.Form):
'quantity', 'quantity',
'currency', '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 company.models import SupplierPart
from stock import models as StockModels from stock import models as StockModels
import common.models
class PartCategory(InvenTreeTree): class PartCategory(InvenTreeTree):
""" PartCategory provides hierarchical organization of Part objects. """ PartCategory provides hierarchical organization of Part objects.
@ -1226,6 +1228,21 @@ class PartAttachment(InvenTreeAttachment):
related_name='attachments') 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): class PartStar(models.Model):
""" A PartStar object creates a relationship between a User and a Part. """ 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 PartParameter, PartParameterTemplate
from .models import PartAttachment from .models import PartAttachment
from .models import PartTestTemplate from .models import PartTestTemplate
from .models import PartSellPriceBreak
from stock.models import StockItem 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): class PartThumbSerializer(serializers.Serializer):
""" """
Serializer for the 'image' field of the Part model. 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> </li>
{% endif %} {% endif %}
{% if part.salable %} {% 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 %}> <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> <a href="{% url 'part-sales-orders' part.id %}">{% trans "Sales Orders" %} <span class='badge'>{{ part.sales_orders|length }}</span></a>
</li> </li>

View File

@ -18,6 +18,12 @@ part_attachment_urls = [
url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), 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 = [ part_parameter_urls = [
url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), 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'^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+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'),
] ]
part_detail_urls = [ 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'^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'^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'^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'^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'^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'),
@ -108,6 +114,9 @@ part_urls = [
# Part attachments # Part attachments
url(r'^attachment/', include(part_attachment_urls)), url(r'^attachment/', include(part_attachment_urls)),
# Part price breaks
url(r'^sale-price/', include(sale_price_break_urls)),
# Part test templates # Part test templates
url(r'^test-template/', include([ url(r'^test-template/', include([
url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'), 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 BomItem
from .models import match_part_names from .models import match_part_names
from .models import PartTestTemplate from .models import PartTestTemplate
from .models import PartSellPriceBreak
from common.models import Currency, InvenTreeSetting from common.models import Currency, InvenTreeSetting
from company.models import SupplierPart from company.models import SupplierPart
@ -2097,3 +2098,75 @@ 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 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) manage(c, 'createsuperuser', pty=True)
@task
def check(c):
"""
Check validity of django codebase
"""
manage(c, "check")
@task @task
def migrate(c): def migrate(c):
""" """