From 4ea4456517b607a8d38b36b75907819d0bdf1962 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 23:56:47 +1100 Subject: [PATCH 01/35] Add API LIST endpoint for SalesOrderAllocations --- InvenTree/order/api.py | 84 ++++++++++++++++--- InvenTree/order/serializers.py | 14 ++-- .../templates/order/sales_order_detail.html | 4 +- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index ce75a47697..700b97f67a 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -22,10 +22,10 @@ from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrderAttachment from .serializers import POSerializer, POLineItemSerializer, POAttachmentSerializer -from .models import SalesOrder, SalesOrderLineItem +from .models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation from .models import SalesOrderAttachment from .serializers import SalesOrderSerializer, SOLineItemSerializer, SOAttachmentSerializer - +from .serializers import SalesOrderAllocationSerializer class POList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PurchaseOrder objects @@ -427,6 +427,56 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView): serializer_class = SOLineItemSerializer +class SOAllocationList(generics.ListCreateAPIView): + """ + API endpoint for listing SalesOrderAllocation objects + """ + + queryset = SalesOrderAllocation.objects.all() + serializer_class = SalesOrderAllocationSerializer + + def filter_queryset(self, queryset): + + queryset = super().filter_queryset(queryset) + + # Filter by order + params = self.request.query_params + + # Filter by "part" reference + part = params.get('part', None) + + if part is not None: + queryset = queryset.filter(item__part=part) + + # Filter by "order" reference + order = params.get('order', None) + + if order is not None: + queryset = queryset.filter(line__order=order) + + # Filter by "outstanding" order status + outstanding = params.get('outstanding', None) + + if outstanding is not None: + outstanding = str2bool(outstanding) + + if outstanding: + queryset = queryset.filter(line__order__status__in=SalesOrderStatus.OPEN) + else: + queryset = queryset.exclude(line__order__status__in=SalesOrderStatus.OPEN) + + return queryset + + filter_backends = [ + DjangoFilterBackend, + ] + + # Default filterable fields + filter_fields = [ + 'item', + ] + + class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload) @@ -435,10 +485,6 @@ class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): queryset = PurchaseOrderAttachment.objects.all() serializer_class = POAttachmentSerializer - filter_fields = [ - 'order', - ] - order_api_urls = [ # API endpoints for purchase orders @@ -453,14 +499,26 @@ order_api_urls = [ url(r'^po-line/$', POLineItemList.as_view(), name='api-po-line-list'), # API endpoints for sales ordesr - url(r'^so/(?P\d+)/$', SODetail.as_view(), name='api-so-detail'), - url(r'so/attachment/', include([ - url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'), + url(r'^so/', include([ + url(r'^(?P\d+)/$', SODetail.as_view(), name='api-so-detail'), + url(r'attachment/', include([ + url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'), + ])), + + # List all sales orders + url(r'^.*$', SOList.as_view(), name='api-so-list'), ])), - url(r'^so/.*$', SOList.as_view(), name='api-so-list'), - # API endpoints for sales order line items - url(r'^so-line/(?P\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'), - url(r'^so-line/$', SOLineItemList.as_view(), name='api-so-line-list'), + url(r'^so-line/', include([ + url(r'^(?P\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'), + url(r'^$', SOLineItemList.as_view(), name='api-so-line-list'), + ])), + + # API endpoints for sales order allocations + url(r'^so-allocation', include([ + + # List all sales order allocations + url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'), + ])), ] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index a04798c303..51a0d6ebf0 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -232,10 +232,12 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): This includes some fields from the related model objects. """ - location_path = serializers.CharField(source='get_location_path') - location_id = serializers.IntegerField(source='get_location') - serial = serializers.CharField(source='get_serial') - quantity = serializers.FloatField() + location_path = serializers.CharField(source='get_location_path', read_only=True) + location = serializers.IntegerField(source='get_location', read_only=True) + part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True) + order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True) + serial = serializers.CharField(source='get_serial', read_only=True) + quantity = serializers.FloatField(read_only=True) class Meta: model = SalesOrderAllocation @@ -245,7 +247,9 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): 'line', 'serial', 'quantity', - 'location_id', + 'order', + 'part', + 'location', 'location_path', 'item', ] diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 8e3128e1f3..d4a004a73c 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -78,10 +78,10 @@ function showAllocationSubTable(index, row, element) { }, }, { - field: 'location_id', + field: 'location', title: 'Location', formatter: function(value, row, index, field) { - return renderLink(row.location_path, `/stock/location/${row.location_id}/`); + return renderLink(row.location_path, `/stock/location/${row.location}/`); }, }, { From 77df82c46d15f8a59a5ea9a32195baf64787817c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 00:07:44 +1100 Subject: [PATCH 02/35] Add optional detail elements to SOAllocation API --- InvenTree/order/api.py | 10 ++++++++++ InvenTree/order/serializers.py | 34 ++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 700b97f67a..1b376e9a32 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -435,6 +435,16 @@ class SOAllocationList(generics.ListCreateAPIView): queryset = SalesOrderAllocation.objects.all() serializer_class = SalesOrderAllocationSerializer + def get_serializer(self, *args, **kwargs): + + params = self.request.query_params + + kwargs['part_detail'] = str2bool(params.get('part_detail', False)) + kwargs['item_detail'] = str2bool(params.get('item_detail', False)) + kwargs['order_detail'] = str2bool(params.get('order_detail', False)) + + return self.serializer_class(*args, **kwargs) + def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 51a0d6ebf0..d9d6143253 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -17,6 +17,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField from company.serializers import CompanyBriefSerializer, SupplierPartSerializer from part.serializers import PartBriefSerializer +from stock.serializers import StockItemSerializer from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrderAttachment, SalesOrderAttachment @@ -232,13 +233,33 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): This includes some fields from the related model objects. """ - location_path = serializers.CharField(source='get_location_path', read_only=True) - location = serializers.IntegerField(source='get_location', read_only=True) part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True) order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True) serial = serializers.CharField(source='get_serial', read_only=True) quantity = serializers.FloatField(read_only=True) + # Extra detail fields + order_detail = SalesOrderSerializer(source='line.order', many=False, read_only=True) + part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True) + item_detail = StockItemSerializer(source='item', many=False, read_only=True) + + def __init__(self, *args, **kwargs): + + order_detail = kwargs.pop('order_detail', False) + part_detail = kwargs.pop('part_detail', False) + item_detail = kwargs.pop('item_detail', False) + + super().__init__(*args, **kwargs) + + if not order_detail: + self.fields.pop('order_detail') + + if not part_detail: + self.fields.pop('part_detail') + + if not item_detail: + self.fields.pop('item_detail') + class Meta: model = SalesOrderAllocation @@ -247,11 +268,12 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): 'line', 'serial', 'quantity', - 'order', - 'part', - 'location', - 'location_path', 'item', + 'item_detail', + 'order', + 'order_detail', + 'part', + 'part_detail', ] From 228349bea615ecc45eacc22bd3cc06df5dcf45bb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 00:36:51 +1100 Subject: [PATCH 03/35] Add 'location_detail' filter --- InvenTree/order/api.py | 1 + InvenTree/order/serializers.py | 10 ++- InvenTree/part/templates/part/allocation.html | 10 +++ InvenTree/templates/js/order.js | 82 +++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 1b376e9a32..54a88f19be 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -442,6 +442,7 @@ class SOAllocationList(generics.ListCreateAPIView): kwargs['part_detail'] = str2bool(params.get('part_detail', False)) kwargs['item_detail'] = str2bool(params.get('item_detail', False)) kwargs['order_detail'] = str2bool(params.get('order_detail', False)) + kwargs['location_detail'] = str2bool(params.get('location_detail', False)) return self.serializer_class(*args, **kwargs) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index d9d6143253..aa6b05fe39 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -17,7 +17,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField from company.serializers import CompanyBriefSerializer, SupplierPartSerializer from part.serializers import PartBriefSerializer -from stock.serializers import StockItemSerializer +from stock.serializers import StockItemSerializer, LocationSerializer from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrderAttachment, SalesOrderAttachment @@ -237,17 +237,20 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True) serial = serializers.CharField(source='get_serial', read_only=True) quantity = serializers.FloatField(read_only=True) + location = serializers.PrimaryKeyRelatedField(source='item.location', many=False, read_only=True) # Extra detail fields order_detail = SalesOrderSerializer(source='line.order', many=False, read_only=True) part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True) item_detail = StockItemSerializer(source='item', many=False, read_only=True) + location_detail = LocationSerializer(source='item.location', many=False, read_only=True) def __init__(self, *args, **kwargs): order_detail = kwargs.pop('order_detail', False) part_detail = kwargs.pop('part_detail', False) item_detail = kwargs.pop('item_detail', False) + location_detail = kwargs.pop('location_detail', False) super().__init__(*args, **kwargs) @@ -260,6 +263,9 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): if not item_detail: self.fields.pop('item_detail') + if not location_detail: + self.fields.pop('location_detail') + class Meta: model = SalesOrderAllocation @@ -268,6 +274,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): 'line', 'serial', 'quantity', + 'location', + 'location_detail', 'item', 'item_detail', 'order', diff --git a/InvenTree/part/templates/part/allocation.html b/InvenTree/part/templates/part/allocation.html index e574742ad5..93acf6ec4a 100644 --- a/InvenTree/part/templates/part/allocation.html +++ b/InvenTree/part/templates/part/allocation.html @@ -9,6 +9,10 @@

{% trans "Part Stock Allocations" %}

+
+ +
+ @@ -35,6 +39,12 @@ {% block js_ready %} + loadSalesOrderAllocationTable("#sales-order-table", { + params: { + part: {{ part.id }}, + } + }); + $("#build-table").inventreeTable({ columns: [ { diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js index 53063cd709..fcaf54ef63 100644 --- a/InvenTree/templates/js/order.js +++ b/InvenTree/templates/js/order.js @@ -304,3 +304,85 @@ function loadSalesOrderTable(table, options) { ], }); } + + +function loadSalesOrderAllocationTable(table, options={}) { + /** + * Load a table with SalesOrderAllocation items + */ + + options.params = options.params || {}; + + options.params['location_detail'] = true; + options.params['part_detail'] = true; + options.params['item_detail'] = true; + options.params['order_detail'] = true; + + var filters = loadTableFilters("salesorderallocation"); + + for (var key in options.params) { + filters[key] = options.params[key]; + } + + setupFilterList("salesorderallocation", $(table)); + + $(table).inventreeTable({ + url: '{% url "api-so-allocation-list" %}', + queryParams: filters, + name: 'salesorderallocation', + groupBy: false, + original: options.params, + formatNoMatches: function() { return '{% trans "No sales order allocations found" %}'; }, + columns: [ + { + field: 'pk', + visible: false, + switchable: false, + }, + { + field: 'order', + title: '{% trans "Order" %}', + switchable: false, + formatter: function(value, row) { + + var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}"; + + var ref = `${prefix}${row.order_detail.reference}`; + + return renderLink(ref, `/order/sales-order/${row.order}/`); + } + }, + { + field: 'item', + title: '{% trans "Stock Item" %}', + formatter: function(value, row) { + // Render a link to the particular stock item + + var link = `/stock/item/${row.item}/`; + var text = `{% trans "Stock Item" %} ${row.item}`; + + return renderLink(text, link); + } + }, + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row) { + + if (!value) { + return '{% trans "Location not specified" %}'; + } + + var link = `/stock/location/${value}`; + var text = row.location_detail.description; + + return renderLink(text, link); + } + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + } + ] + }); +} \ No newline at end of file From 0d93c96f2af6a4f7462ee98fe0754fcfc363f541 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Jun 2021 17:01:49 +0200 Subject: [PATCH 04/35] adding internal price breaks as in #1606 --- InvenTree/part/admin.py | 11 +- InvenTree/part/api.py | 24 +++- InvenTree/part/forms.py | 18 ++- .../migrations/0067_partinternalpricebreak.py | 30 +++++ InvenTree/part/models.py | 15 +++ InvenTree/part/serializers.py | 22 +++- .../part/templates/part/internal_prices.html | 108 ++++++++++++++++++ InvenTree/part/templates/part/navbar.html | 6 + InvenTree/part/urls.py | 10 ++ InvenTree/part/views.py | 25 +++- 10 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 InvenTree/part/migrations/0067_partinternalpricebreak.py create mode 100644 InvenTree/part/templates/part/internal_prices.html diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 3945b56d7c..637dbbf2cf 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -14,7 +14,7 @@ from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartCategoryParameterTemplate from .models import PartTestTemplate -from .models import PartSellPriceBreak +from .models import PartSellPriceBreak, PartInternalPriceBreak from stock.models import StockLocation from company.models import SupplierPart @@ -286,6 +286,14 @@ class PartSellPriceBreakAdmin(admin.ModelAdmin): 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(PartCategory, PartCategoryAdmin) admin.site.register(PartRelated, PartRelatedAdmin) @@ -297,3 +305,4 @@ admin.site.register(PartParameter, ParameterAdmin) admin.site.register(PartCategoryParameterTemplate, PartCategoryParameterAdmin) admin.site.register(PartTestTemplate, PartTestTemplateAdmin) admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin) +admin.site.register(PartInternalPriceBreak, PartInternalPriceBreakAdmin) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 537b0f9e40..8883699f9a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -25,7 +25,7 @@ from django.urls import reverse from .models import Part, PartCategory, BomItem from .models import PartParameter, PartParameterTemplate from .models import PartAttachment, PartTestTemplate -from .models import PartSellPriceBreak +from .models import PartSellPriceBreak, PartInternalPriceBreak from .models import PartCategoryParameterTemplate 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): """ 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'), ])), + # 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 url(r'^parameter/', include([ url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'), diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 95de4961f9..ec799bcf8d 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -20,7 +20,7 @@ from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartCategoryParameterTemplate from .models import PartTestTemplate -from .models import PartSellPriceBreak +from .models import PartSellPriceBreak, PartInternalPriceBreak class PartModelChoiceField(forms.ModelChoiceField): @@ -394,3 +394,19 @@ class EditPartSalePriceBreakForm(HelperForm): 'quantity', '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', + ] diff --git a/InvenTree/part/migrations/0067_partinternalpricebreak.py b/InvenTree/part/migrations/0067_partinternalpricebreak.py new file mode 100644 index 0000000000..f1b16fe87c --- /dev/null +++ b/InvenTree/part/migrations/0067_partinternalpricebreak.py @@ -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')}, + }, + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 7b9038fecb..a09e6db217 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1983,6 +1983,21 @@ class PartSellPriceBreak(common.models.PriceBreak): 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): """ A PartStar object creates a relationship between a User and a Part. diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index d03a37c6dc..76275bd5e1 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -17,7 +17,8 @@ from stock.models import StockItem from .models import (BomItem, Part, PartAttachment, PartCategory, PartParameter, PartParameterTemplate, PartSellPriceBreak, - PartStar, PartTestTemplate, PartCategoryParameterTemplate) + PartStar, PartTestTemplate, PartCategoryParameterTemplate, + PartInternalPriceBreak) 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): """ Serializer for the 'image' field of the Part model. diff --git a/InvenTree/part/templates/part/internal_prices.html b/InvenTree/part/templates/part/internal_prices.html new file mode 100644 index 0000000000..dbf3986943 --- /dev/null +++ b/InvenTree/part/templates/part/internal_prices.html @@ -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 %} + +
+ +
+ +
{% trans "Order" %}
+
+ +{% 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 += `
` + + 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 += `
`; + + return html; + } + }, + ] +}) + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html index a2c8104486..f8f7d488b0 100644 --- a/InvenTree/part/templates/part/navbar.html +++ b/InvenTree/part/templates/part/navbar.html @@ -95,6 +95,12 @@ {% endif %} {% if part.salable and roles.sales_order.view %} +
  • + + + {% trans "Internal Price" %} + +
  • diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index e53ce54782..d12612c604 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -29,6 +29,12 @@ sale_price_break_urls = [ url(r'^(?P\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\d+)/edit/', views.PartInternalPriceBreakEdit.as_view(), name='internal-price-break-edit'), + url(r'^(?P\d+)/delete/', views.PartInternalPriceBreakDelete.as_view(), name='internal-price-break-delete'), +] + part_parameter_urls = [ url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), url(r'^template/(?P\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'^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'^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'^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'), @@ -145,6 +152,9 @@ part_urls = [ # Part price breaks 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 url(r'^test-template/', include([ url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index b4f33c9382..5927351124 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -36,7 +36,7 @@ from .models import PartCategoryParameterTemplate from .models import BomItem from .models import match_part_names from .models import PartTestTemplate -from .models import PartSellPriceBreak +from .models import PartSellPriceBreak, PartInternalPriceBreak from common.models import InvenTreeSetting from company.models import SupplierPart @@ -2794,3 +2794,26 @@ class PartSalePriceBreakDelete(AjaxDeleteView): model = PartSellPriceBreak ajax_form_title = _("Delete Price Break") 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") From 768080f9a002761980b08ffaa02c3a807f8dc3aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Jun 2021 17:06:58 +0200 Subject: [PATCH 05/35] Adding internal functions to use internal prices --- InvenTree/common/models.py | 9 ++++++--- InvenTree/part/models.py | 16 ++++++++++++++++ InvenTree/part/templates/part/order_prices.html | 13 +++++++++++++ InvenTree/part/views.py | 6 ++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 236e48770f..f08aad1c68 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -726,7 +726,7 @@ class PriceBreak(models.Model): return converted.amount -def get_price(instance, quantity, moq=True, multiples=True, currency=None): +def get_price(instance, quantity, moq=True, multiples=True, currency=None, break_name:str='price_breaks'): """ Calculate the price based on quantity price breaks. - Don't forget to add in flat-fee cost (base_cost field) @@ -734,7 +734,10 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None): - If order multiples are to be observed, then we need to calculate based on that, too """ - price_breaks = instance.price_breaks.all() + if hasattr(instance, break_name): + price_breaks = getattr(instance, break_name).all() + else: + price_breaks = [] # No price break information available? if len(price_breaks) == 0: @@ -756,7 +759,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None): currency = currency_code_default() pb_min = None - for pb in instance.price_breaks.all(): + for pb in price_breaks: # Store smallest price break if not pb_min: pb_min = pb diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index a09e6db217..9ff4938891 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1649,6 +1649,22 @@ class Part(MPTTModel): price=price ) + def get_internal_price(instance, quantity, moq=True, multiples=True, currency=None): + return common.models.get_price(instance, quantity, moq, multiples, currency, break_name='internal_price_breaks') + + @property + def has_internal_price_breaks(self): + return self.internal_price_breaks.count() > 0 + + @property + def internal_price_breaks(self): + """ Return the associated price breaks in the correct order """ + return self.internalpricebreaks.order_by('quantity').all() + + @property + def internal_unit_pricing(self): + return self.get_internal_price(1) + @transaction.atomic def copy_bom_from(self, other, clear=True, **kwargs): """ diff --git a/InvenTree/part/templates/part/order_prices.html b/InvenTree/part/templates/part/order_prices.html index 6c5b2173bf..23644ef5b8 100644 --- a/InvenTree/part/templates/part/order_prices.html +++ b/InvenTree/part/templates/part/order_prices.html @@ -77,6 +77,19 @@ {% endif %} {% endif %} +{% if total_internal_part_price %} + + {% trans 'Internal Price' %} + {% trans 'Unit Cost' %} + {% include "price.html" with price=unit_internal_part_price %} + + + + {% trans 'Total Cost' %} + {% include "price.html" with price=total_internal_part_price %} + +{% endif %} + {% if total_part_price %} {% trans 'Sale Price' %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 5927351124..ee7733a6be 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2127,6 +2127,12 @@ class PartPricing(AjaxView): ctx['max_total_bom_price'] = max_bom_price ctx['max_unit_bom_price'] = max_unit_bom_price + # internal part pricing information + internal_part_price = part.get_internal_price(quantity) + if internal_part_price is not None: + ctx['total_internal_part_price'] = round(internal_part_price, 3) + ctx['unit_internal_part_price'] = round(internal_part_price / quantity, 3) + # part pricing information part_price = part.get_price(quantity) if part_price is not None: From 81f00753c67a0377b9b37c1c304b4ef7ae6c600a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Jun 2021 17:24:04 +0200 Subject: [PATCH 06/35] style --- InvenTree/common/models.py | 2 +- InvenTree/part/models.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index f08aad1c68..7d4b69f7a6 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -726,7 +726,7 @@ class PriceBreak(models.Model): return converted.amount -def get_price(instance, quantity, moq=True, multiples=True, currency=None, break_name:str='price_breaks'): +def get_price(instance, quantity, moq=True, multiples=True, currency=None, break_name: str = 'price_breaks'): """ Calculate the price based on quantity price breaks. - Don't forget to add in flat-fee cost (base_cost field) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 9ff4938891..7133048527 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1649,8 +1649,8 @@ class Part(MPTTModel): price=price ) - def get_internal_price(instance, quantity, moq=True, multiples=True, currency=None): - return common.models.get_price(instance, quantity, moq, multiples, currency, break_name='internal_price_breaks') + def get_internal_price(self, quantity, moq=True, multiples=True, currency=None): + return common.models.get_price(self, quantity, moq, multiples, currency, break_name='internal_price_breaks') @property def has_internal_price_breaks(self): From 06d3489ede4a4ca6891efa19247376c336c67dac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Jun 2021 18:06:26 +0200 Subject: [PATCH 07/35] fix: added ruleset --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index f26a7ee64b..0902b04ccd 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -77,6 +77,7 @@ class RuleSet(models.Model): 'part_bomitem', 'part_partattachment', 'part_partsellpricebreak', + 'part_partinternalpricebreak', 'part_parttesttemplate', 'part_partparametertemplate', 'part_partparameter', From 6ae9fa716c46dbf3471503f48a5b24b3efe26831 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Jun 2021 00:19:37 +0200 Subject: [PATCH 08/35] added internal price to part_pricing --- InvenTree/part/templates/part/part_pricing.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InvenTree/part/templates/part/part_pricing.html b/InvenTree/part/templates/part/part_pricing.html index e035a77162..cd5db9619c 100644 --- a/InvenTree/part/templates/part/part_pricing.html +++ b/InvenTree/part/templates/part/part_pricing.html @@ -74,6 +74,20 @@ {% endif %} +{% if total_internal_part_price %} +

    {% trans 'Internal Price' %}

    + + + + + + + + + +
    {% trans 'Unit Cost' %}{% include "price.html" with price=unit_internal_part_price %}
    {% trans 'Total Cost' %}{% include "price.html" with price=total_internal_part_price %}
    +{% endif %} + {% if total_part_price %}

    {% trans 'Sale Price' %}

    From 62638f76ede8218f802ce232a8f15e3a20b04aaf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Jun 2021 04:00:12 +0200 Subject: [PATCH 09/35] fixing wrong page title --- InvenTree/part/templates/part/internal_prices.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/internal_prices.html b/InvenTree/part/templates/part/internal_prices.html index dbf3986943..5f37697383 100644 --- a/InvenTree/part/templates/part/internal_prices.html +++ b/InvenTree/part/templates/part/internal_prices.html @@ -7,7 +7,7 @@ {% endblock %} {% block heading %} -{% trans "Sell Price Information" %} +{% trans "Internal Price Information" %} {% endblock %} {% block details %} From 37c002539958a999c74f2eb508108bf71573edf5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Jun 2021 04:58:37 +0200 Subject: [PATCH 10/35] settings for internal prices added --- InvenTree/common/models.py | 14 ++++++++++++++ InvenTree/part/templates/part/internal_prices.html | 8 +++++++- InvenTree/part/templates/part/navbar.html | 4 +++- InvenTree/templates/InvenTree/settings/part.html | 3 +++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 62c17593d8..066f028236 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -205,6 +205,20 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'PART_INTERNAL_PRICE': { + 'name': _('Internal Prices'), + 'description': _('Set internal prices for parts'), + 'default': False, + 'validator': bool + }, + + 'PART_BOM_USE_INTERNAL_PRICE': { + 'name': _('Internal Prices in BOM-Price'), + 'description': _('Use the internal price (if set) for BOM-price calculations'), + 'default': False, + 'validator': bool + }, + 'REPORT_DEBUG_MODE': { 'name': _('Debug Mode'), 'description': _('Generate reports in debug mode (HTML output)'), diff --git a/InvenTree/part/templates/part/internal_prices.html b/InvenTree/part/templates/part/internal_prices.html index 5f37697383..fdc69ce198 100644 --- a/InvenTree/part/templates/part/internal_prices.html +++ b/InvenTree/part/templates/part/internal_prices.html @@ -1,6 +1,7 @@ {% extends "part/part_base.html" %} {% load static %} {% load i18n %} +{% load inventree_extras %} {% block menubar %} {% include 'part/navbar.html' with tab='internal-prices' %} @@ -11,7 +12,8 @@ {% endblock %} {% block details %} - +{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} +{% if show_internal_price %}
    +{% endif %} {% endblock %} {% block js_ready %} {{ block.super }} +{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} +{% if show_internal_price %} function reloadPriceBreaks() { $("#internal-price-break-table").bootstrapTable("refresh"); } @@ -105,4 +110,5 @@ $('#internal-price-break-table').inventreeTable({ ] }) +{% endif %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html index a2ddd9be0a..1e5c5a74de 100644 --- a/InvenTree/part/templates/part/navbar.html +++ b/InvenTree/part/templates/part/navbar.html @@ -2,6 +2,8 @@ {% load static %} {% load inventree_extras %} +{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} +