mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
7dbb6c7c8e
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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():
|
||||||
|
@ -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')
|
||||||
)
|
)
|
||||||
|
@ -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 """
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
@ -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>
|
</div>
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
|
||||||
<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 %}
|
|
||||||
</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 %}
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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'),
|
||||||
|
@ -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',
|
||||||
|
]
|
||||||
|
30
InvenTree/part/migrations/0049_partsellpricebreak.py
Normal file
30
InvenTree/part/migrations/0049_partsellpricebreak.py
Normal 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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
19
InvenTree/part/migrations/0050_auto_20200917_2315.py
Normal file
19
InvenTree/part/migrations/0050_auto_20200917_2315.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
110
InvenTree/part/templates/part/sale_prices.html
Normal file
110
InvenTree/part/templates/part/sale_prices.html
Normal 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 %}
|
@ -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>
|
||||||
|
@ -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'),
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user