mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
adding internal price breaks as in #1606
This commit is contained in:
parent
92f77b6bbb
commit
0d93c96f2a
@ -14,7 +14,7 @@ from .models import BomItem
|
|||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import PartTestTemplate
|
from .models import PartTestTemplate
|
||||||
from .models import PartSellPriceBreak
|
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||||
|
|
||||||
from stock.models import StockLocation
|
from stock.models import StockLocation
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
@ -286,6 +286,14 @@ class PartSellPriceBreakAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('part', 'quantity', 'price',)
|
list_display = ('part', 'quantity', 'price',)
|
||||||
|
|
||||||
|
|
||||||
|
class PartInternalPriceBreakAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartInternalPriceBreak
|
||||||
|
|
||||||
|
list_display = ('part', 'quantity', 'price',)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Part, PartAdmin)
|
admin.site.register(Part, PartAdmin)
|
||||||
admin.site.register(PartCategory, PartCategoryAdmin)
|
admin.site.register(PartCategory, PartCategoryAdmin)
|
||||||
admin.site.register(PartRelated, PartRelatedAdmin)
|
admin.site.register(PartRelated, PartRelatedAdmin)
|
||||||
@ -297,3 +305,4 @@ admin.site.register(PartParameter, ParameterAdmin)
|
|||||||
admin.site.register(PartCategoryParameterTemplate, PartCategoryParameterAdmin)
|
admin.site.register(PartCategoryParameterTemplate, PartCategoryParameterAdmin)
|
||||||
admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
|
admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
|
||||||
admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin)
|
admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin)
|
||||||
|
admin.site.register(PartInternalPriceBreak, PartInternalPriceBreakAdmin)
|
||||||
|
@ -25,7 +25,7 @@ from django.urls import reverse
|
|||||||
from .models import Part, PartCategory, BomItem
|
from .models import Part, PartCategory, BomItem
|
||||||
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 .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
@ -194,6 +194,23 @@ class PartSalePriceList(generics.ListCreateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartInternalPriceList(generics.ListCreateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for list view of PartInternalPriceBreak model
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = PartInternalPriceBreak.objects.all()
|
||||||
|
serializer_class = part_serializers.PartInternalPriceSerializer
|
||||||
|
|
||||||
|
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).
|
||||||
@ -1017,6 +1034,11 @@ part_api_urls = [
|
|||||||
url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
|
url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
# Base URL for part internal pricing
|
||||||
|
url(r'^internal-price/', include([
|
||||||
|
url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-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'),
|
||||||
|
@ -20,7 +20,7 @@ from .models import BomItem
|
|||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import PartTestTemplate
|
from .models import PartTestTemplate
|
||||||
from .models import PartSellPriceBreak
|
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||||
|
|
||||||
|
|
||||||
class PartModelChoiceField(forms.ModelChoiceField):
|
class PartModelChoiceField(forms.ModelChoiceField):
|
||||||
@ -394,3 +394,19 @@ class EditPartSalePriceBreakForm(HelperForm):
|
|||||||
'quantity',
|
'quantity',
|
||||||
'price',
|
'price',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EditPartInternalPriceBreakForm(HelperForm):
|
||||||
|
"""
|
||||||
|
Form for creating / editing a internal price for a part
|
||||||
|
"""
|
||||||
|
|
||||||
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartInternalPriceBreak
|
||||||
|
fields = [
|
||||||
|
'part',
|
||||||
|
'quantity',
|
||||||
|
'price',
|
||||||
|
]
|
||||||
|
30
InvenTree/part/migrations/0067_partinternalpricebreak.py
Normal file
30
InvenTree/part/migrations/0067_partinternalpricebreak.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 3.2 on 2021-06-05 14:13
|
||||||
|
|
||||||
|
import InvenTree.fields
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import djmoney.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0066_bomitem_allow_variants'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PartInternalPriceBreak',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Price break quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Quantity')),
|
||||||
|
('price_currency', djmoney.models.fields.CurrencyField(choices=[('AUD', 'Australian Dollar'), ('CAD', 'Canadian Dollar'), ('EUR', 'Euro'), ('NZD', 'New Zealand Dollar'), ('GBP', 'Pound Sterling'), ('USD', 'US Dollar'), ('JPY', 'Yen')], default='USD', editable=False, max_length=3)),
|
||||||
|
('price', djmoney.models.fields.MoneyField(decimal_places=4, default_currency='USD', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price')),
|
||||||
|
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='internalpricebreaks', to='part.part', verbose_name='Part')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('part', 'quantity')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1983,6 +1983,21 @@ class PartSellPriceBreak(common.models.PriceBreak):
|
|||||||
unique_together = ('part', 'quantity')
|
unique_together = ('part', 'quantity')
|
||||||
|
|
||||||
|
|
||||||
|
class PartInternalPriceBreak(common.models.PriceBreak):
|
||||||
|
"""
|
||||||
|
Represents a price break for internally selling this part
|
||||||
|
"""
|
||||||
|
|
||||||
|
part = models.ForeignKey(
|
||||||
|
Part, on_delete=models.CASCADE,
|
||||||
|
related_name='internalpricebreaks',
|
||||||
|
verbose_name=_('Part')
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@ from stock.models import StockItem
|
|||||||
|
|
||||||
from .models import (BomItem, Part, PartAttachment, PartCategory,
|
from .models import (BomItem, Part, PartAttachment, PartCategory,
|
||||||
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
||||||
PartStar, PartTestTemplate, PartCategoryParameterTemplate)
|
PartStar, PartTestTemplate, PartCategoryParameterTemplate,
|
||||||
|
PartInternalPriceBreak)
|
||||||
|
|
||||||
|
|
||||||
class CategorySerializer(InvenTreeModelSerializer):
|
class CategorySerializer(InvenTreeModelSerializer):
|
||||||
@ -100,6 +101,25 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartInternalPriceSerializer(InvenTreeModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for internal prices for Part model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
|
price = serializers.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartInternalPriceBreak
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'part',
|
||||||
|
'quantity',
|
||||||
|
'price',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
108
InvenTree/part/templates/part/internal_prices.html
Normal file
108
InvenTree/part/templates/part/internal_prices.html
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
{% extends "part/part_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include 'part/navbar.html' with tab='internal-prices' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Sell Price Information" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
<div id='internal-price-break-toolbar' class='btn-group'>
|
||||||
|
<button class='btn btn-primary' id='new-internal-price-break' type='button'>
|
||||||
|
<span class='fas fa-plus-circle'></span> {% trans "Add Internal Price Break" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' id='internal-price-break-table' data-toolbar='#internal-price-break-toolbar'>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
function reloadPriceBreaks() {
|
||||||
|
$("#internal-price-break-table").bootstrapTable("refresh");
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#new-internal-price-break').click(function() {
|
||||||
|
launchModalForm("{% url 'internal-price-break-create' %}",
|
||||||
|
{
|
||||||
|
success: reloadPriceBreaks,
|
||||||
|
data: {
|
||||||
|
part: {{ part.id }},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#internal-price-break-table').inventreeTable({
|
||||||
|
name: 'internalprice',
|
||||||
|
formatNoMatches: function() { return "{% trans 'No internal price break information found' %}"; },
|
||||||
|
queryParams: {
|
||||||
|
part: {{ part.id }},
|
||||||
|
},
|
||||||
|
url: "{% url 'api-part-internal-price-list' %}",
|
||||||
|
onPostBody: function() {
|
||||||
|
var table = $('#internal-price-break-table');
|
||||||
|
|
||||||
|
table.find('.button-internal-price-break-delete').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
launchModalForm(
|
||||||
|
`/part/internal-price/${pk}/delete/`,
|
||||||
|
{
|
||||||
|
success: reloadPriceBreaks
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.find('.button-internal-price-break-edit').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
launchModalForm(
|
||||||
|
`/part/internal-price/${pk}/edit/`,
|
||||||
|
{
|
||||||
|
success: reloadPriceBreaks
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'quantity',
|
||||||
|
title: '{% trans "Quantity" %}',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'price',
|
||||||
|
title: '{% trans "Price" %}',
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row, index) {
|
||||||
|
var html = value;
|
||||||
|
|
||||||
|
html += `<div class='btn-group float-right' role='group'>`
|
||||||
|
|
||||||
|
html += makeIconButton('fa-edit icon-blue', 'button-internal-price-break-edit', row.pk, '{% trans "Edit internal price break" %}');
|
||||||
|
html += makeIconButton('fa-trash-alt icon-red', 'button-internal-price-break-delete', row.pk, '{% trans "Delete internal price break" %}');
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -95,6 +95,12 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.salable and roles.sales_order.view %}
|
{% if part.salable and roles.sales_order.view %}
|
||||||
|
<li class='list-group-item {% if tab == "internal-prices" %}active{% endif %}' title='{% trans "Internal Price Information" %}'>
|
||||||
|
<a href='{% url "part-internal-prices" part.id %}'>
|
||||||
|
<span class='menu-tab-icon fas fa-dollar-sign' style='width: 20px;'></span>
|
||||||
|
{% trans "Internal Price" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class='list-group-item {% if tab == "sales-prices" %}active{% endif %}' title='{% trans "Sales Price Information" %}'>
|
<li class='list-group-item {% if tab == "sales-prices" %}active{% endif %}' title='{% trans "Sales Price Information" %}'>
|
||||||
<a href='{% url "part-sale-prices" part.id %}'>
|
<a href='{% url "part-sale-prices" part.id %}'>
|
||||||
<span class='menu-tab-icon fas fa-dollar-sign' style='width: 20px;'></span>
|
<span class='menu-tab-icon fas fa-dollar-sign' style='width: 20px;'></span>
|
||||||
|
@ -29,6 +29,12 @@ sale_price_break_urls = [
|
|||||||
url(r'^(?P<pk>\d+)/delete/', views.PartSalePriceBreakDelete.as_view(), name='sale-price-break-delete'),
|
url(r'^(?P<pk>\d+)/delete/', views.PartSalePriceBreakDelete.as_view(), name='sale-price-break-delete'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
internal_price_break_urls = [
|
||||||
|
url(r'^new/', views.PartInternalPriceBreakCreate.as_view(), name='internal-price-break-create'),
|
||||||
|
url(r'^(?P<pk>\d+)/edit/', views.PartInternalPriceBreakEdit.as_view(), name='internal-price-break-edit'),
|
||||||
|
url(r'^(?P<pk>\d+)/delete/', views.PartInternalPriceBreakDelete.as_view(), name='internal-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'),
|
||||||
url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'),
|
url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'),
|
||||||
@ -65,6 +71,7 @@ part_detail_urls = [
|
|||||||
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'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'),
|
||||||
|
url(r'^internal-prices/', views.PartDetail.as_view(template_name='part/internal_prices.html'), name='part-internal-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'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'),
|
url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'),
|
||||||
@ -145,6 +152,9 @@ part_urls = [
|
|||||||
# Part price breaks
|
# Part price breaks
|
||||||
url(r'^sale-price/', include(sale_price_break_urls)),
|
url(r'^sale-price/', include(sale_price_break_urls)),
|
||||||
|
|
||||||
|
# Part internal price breaks
|
||||||
|
url(r'^internal-price/', include(internal_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'),
|
||||||
|
@ -36,7 +36,7 @@ from .models import PartCategoryParameterTemplate
|
|||||||
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 .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
@ -2794,3 +2794,26 @@ class PartSalePriceBreakDelete(AjaxDeleteView):
|
|||||||
model = PartSellPriceBreak
|
model = PartSellPriceBreak
|
||||||
ajax_form_title = _("Delete Price Break")
|
ajax_form_title = _("Delete Price Break")
|
||||||
ajax_template_name = "modal_delete_form.html"
|
ajax_template_name = "modal_delete_form.html"
|
||||||
|
|
||||||
|
|
||||||
|
class PartInternalPriceBreakCreate(PartSalePriceBreakCreate):
|
||||||
|
""" View for creating a internal price break for a part """
|
||||||
|
|
||||||
|
model = PartInternalPriceBreak
|
||||||
|
form_class = part_forms.EditPartInternalPriceBreakForm
|
||||||
|
ajax_form_title = _('Add Internal Price Break')
|
||||||
|
|
||||||
|
|
||||||
|
class PartInternalPriceBreakEdit(PartSalePriceBreakEdit):
|
||||||
|
""" View for editing a internal price break """
|
||||||
|
|
||||||
|
model = PartInternalPriceBreak
|
||||||
|
form_class = part_forms.EditPartInternalPriceBreakForm
|
||||||
|
ajax_form_title = _('Edit Internal Price Break')
|
||||||
|
|
||||||
|
|
||||||
|
class PartInternalPriceBreakDelete(PartSalePriceBreakDelete):
|
||||||
|
""" View for deleting a internal price break """
|
||||||
|
|
||||||
|
model = PartInternalPriceBreak
|
||||||
|
ajax_form_title = _("Delete Internal Price Break")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user