mirror of
https://github.com/inventree/InvenTree.git
synced 2025-08-08 04:40:57 +00:00
@@ -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)
|
||||
|
@@ -20,6 +20,7 @@ from django.urls import reverse
|
||||
from .models import Part, PartCategory, BomItem, PartStar
|
||||
from .models import PartParameter, PartParameterTemplate
|
||||
from .models import PartAttachment, PartTestTemplate
|
||||
from .models import PartSellPriceBreak
|
||||
|
||||
from . import serializers as part_serializers
|
||||
|
||||
@@ -107,6 +108,27 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
queryset = PartCategory.objects.all()
|
||||
|
||||
|
||||
class PartSalePriceList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for list view of PartSalePriceBreak model
|
||||
"""
|
||||
|
||||
queryset = PartSellPriceBreak.objects.all()
|
||||
serializer_class = part_serializers.PartSalePriceSerializer
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'part',
|
||||
]
|
||||
|
||||
|
||||
class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
"""
|
||||
API endpoint for listing (and creating) a PartAttachment (file upload).
|
||||
@@ -809,6 +831,11 @@ part_api_urls = [
|
||||
url(r'^(?P<pk>\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'),
|
||||
url(r'^$', PartStarList.as_view(), name='api-part-star-list'),
|
||||
])),
|
||||
|
||||
# Base URL for part sale pricing
|
||||
url(r'^sale-price/', include([
|
||||
url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
|
||||
])),
|
||||
|
||||
# Base URL for PartParameter API endpoints
|
||||
url(r'^parameter/', include([
|
||||
|
@@ -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',
|
||||
]
|
||||
|
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 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.
|
||||
|
||||
|
@@ -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.
|
||||
|
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>
|
||||
{% 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>
|
||||
|
@@ -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'),
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user