diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index a49209dfe7..1ef1df6a8f 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -4,11 +4,15 @@ InvenTree API version information # InvenTree API version -INVENTREE_API_VERSION = 42 +INVENTREE_API_VERSION = 43 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v43 -> 2022-04-26 : https://github.com/inventree/InvenTree/pull/2875 + - Adds API detail endpoint for PartSalePrice model + - Adds API detail endpoint for PartInternalPrice model + v42 -> 2022-04-26 : https://github.com/inventree/InvenTree/pull/2833 - Adds variant stock information to the Part and BomItem serializers diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index b025791a7f..1a80c87322 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -262,6 +262,15 @@ class CategoryTree(generics.ListAPIView): ordering = ['level', 'name'] +class PartSalePriceDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Detail endpoint for PartSellPriceBreak model + """ + + queryset = PartSellPriceBreak.objects.all() + serializer_class = part_serializers.PartSalePriceSerializer + + class PartSalePriceList(generics.ListCreateAPIView): """ API endpoint for list view of PartSalePriceBreak model @@ -279,6 +288,15 @@ class PartSalePriceList(generics.ListCreateAPIView): ] +class PartInternalPriceDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Detail endpoint for PartInternalPriceBreak model + """ + + queryset = PartInternalPriceBreak.objects.all() + serializer_class = part_serializers.PartInternalPriceSerializer + + class PartInternalPriceList(generics.ListCreateAPIView): """ API endpoint for list view of PartInternalPriceBreak model @@ -1920,11 +1938,13 @@ part_api_urls = [ # Base URL for part sale pricing url(r'^sale-price/', include([ + url(r'^(?P\d+)/', PartSalePriceDetail.as_view(), name='api-part-sale-price-detail'), url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'), ])), # Base URL for part internal pricing url(r'^internal-price/', include([ + url(r'^(?P\d+)/', PartInternalPriceDetail.as_view(), name='api-part-internal-price-detail'), url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'), ])), diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 387d3df1d5..0e865ea74b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -17,6 +17,8 @@ from rest_framework import serializers from sql_util.utils import SubqueryCount, SubquerySum from djmoney.contrib.django_rest_framework import MoneyField +from common.settings import currency_code_default, currency_code_mappings + from InvenTree.serializers import (DataFileUploadSerializer, DataFileExtractSerializer, InvenTreeAttachmentSerializerField, @@ -148,6 +150,13 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): allow_null=True ) + price_currency = serializers.ChoiceField( + choices=currency_code_mappings(), + default=currency_code_default, + label=_('Currency'), + help_text=_('Purchase currency of this stock item'), + ) + price_string = serializers.CharField(source='price', read_only=True) class Meta: @@ -157,6 +166,7 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): 'part', 'quantity', 'price', + 'price_currency', 'price_string', ] @@ -172,6 +182,13 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer): allow_null=True ) + price_currency = serializers.ChoiceField( + choices=currency_code_mappings(), + default=currency_code_default, + label=_('Currency'), + help_text=_('Purchase currency of this stock item'), + ) + price_string = serializers.CharField(source='price', read_only=True) class Meta: @@ -181,6 +198,7 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer): 'part', 'quantity', 'price', + 'price_currency', 'price_string', ] diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 2f13faab8a..5ec1821b3d 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -124,8 +124,7 @@ -{% settings_value "PART_SHOW_PRICE_HISTORY" as show_price_history %} -{% if show_price_history %} +{% if part.purchaseable or part.salable %}
{% include "part/prices.html" %}
@@ -1009,7 +1008,7 @@ pb_url_slug: 'internal-price', pb_url: '{% url 'api-part-internal-price-list' %}', pb_new_btn: $('#new-internal-price-break'), - pb_new_url: '{% url 'internal-price-break-create' %}', + pb_new_url: '{% url 'api-part-internal-price-list' %}', linkedGraph: $('#InternalPriceBreakChart'), }, ); @@ -1025,7 +1024,7 @@ pb_url_slug: 'sale-price', pb_url: "{% url 'api-part-sale-price-list' %}", pb_new_btn: $('#new-price-break'), - pb_new_url: '{% url 'sale-price-break-create' %}', + pb_new_url: '{% url 'api-part-sale-price-list' %}', linkedGraph: $('#SalePriceBreakChart'), }, ); diff --git a/InvenTree/part/templates/part/part_sidebar.html b/InvenTree/part/templates/part/part_sidebar.html index 3c1c647f86..e8763fb973 100644 --- a/InvenTree/part/templates/part/part_sidebar.html +++ b/InvenTree/part/templates/part/part_sidebar.html @@ -4,7 +4,6 @@ {% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} {% settings_value 'PART_SHOW_RELATED' as show_related %} -{% settings_value "PART_SHOW_PRICE_HISTORY" as show_price_history %} {% trans "Parameters" as text %} {% include "sidebar_item.html" with label="part-parameters" text=text icon="fa-th-list" %} @@ -28,7 +27,7 @@ {% trans "Used In" as text %} {% include "sidebar_item.html" with label="used-in" text=text icon="fa-layer-group" %} {% endif %} -{% if show_price_history %} +{% if part.purchaseable or part.salable %} {% trans "Pricing" as text %} {% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %} {% endif %} diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 9b829c179b..d47f49ef76 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -3,6 +3,9 @@ {% load crispy_forms_tags %} {% load inventree_extras %} +{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} + +{% if show_price_history %}

{% trans "Pricing Information" %}

@@ -43,7 +46,7 @@ {% endif %} {% endif %} - {% if part.bom_count > 0 %} + {% if part.assembly and part.bom_count > 0 %} {% if min_total_bom_price %} {% trans 'BOM Pricing' %} @@ -147,7 +150,7 @@ -{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} +{% endif %} {% if part.purchaseable and roles.purchase_order.view %} @@ -170,7 +173,7 @@ -{% if price_history %} +{% if show_price_history %}

{% trans "Purchase Price" %} @@ -279,6 +282,7 @@

+{% if show_price_history %}

{% trans "Sale Price" %} @@ -298,3 +302,5 @@ {% endif %}

{% endif %} +{% endif %} + \ No newline at end of file diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 55a3dc52eb..04d2b0a5f8 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -13,18 +13,6 @@ from django.conf.urls import url, include from . import views -sale_price_break_urls = [ - url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'), - url(r'^(?P\d+)/edit/', views.PartSalePriceBreakEdit.as_view(), name='sale-price-break-edit'), - 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'), @@ -86,12 +74,6 @@ part_urls = [ # Part category url(r'^category/', include(category_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 parameters url(r'^parameter/', include(part_parameter_urls)), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 9f3cd07f7c..41e734ce2a 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -18,7 +18,6 @@ from django.forms import HiddenInput from django.conf import settings from django.contrib import messages -from moneyed import CURRENCIES from djmoney.contrib.exchange.models import convert_money from djmoney.contrib.exchange.exceptions import MissingRate @@ -33,7 +32,6 @@ from decimal import Decimal from .models import PartCategory, Part from .models import PartParameterTemplate from .models import PartCategoryParameterTemplate -from .models import PartSellPriceBreak, PartInternalPriceBreak from common.models import InvenTreeSetting from company.models import SupplierPart @@ -389,8 +387,12 @@ class PartDetail(InvenTreeRoleMixin, DetailView): context.update(**ctx) + show_price_history = InvenTreeSetting.get_setting('PART_SHOW_PRICE_HISTORY', False) + + context['show_price_history'] = show_price_history + # Pricing information - if InvenTreeSetting.get_setting('PART_SHOW_PRICE_HISTORY', False): + if show_price_history: ctx = self.get_pricing(self.get_quantity()) ctx['form'] = self.form_class(initial=self.get_initials()) @@ -1226,102 +1228,3 @@ class CategoryParameterTemplateDelete(AjaxDeleteView): return None return self.object - - -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() - - default_currency = inventree_settings.currency_code_default() - currency = CURRENCIES.get(default_currency, None) - - if currency is not None: - initials['price'] = [1.0, currency] - - 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" - - -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') - permission_required = 'roles.sales_order.add' - - -class PartInternalPriceBreakEdit(PartSalePriceBreakEdit): - """ View for editing a internal price break """ - - model = PartInternalPriceBreak - form_class = part_forms.EditPartInternalPriceBreakForm - ajax_form_title = _('Edit Internal Price Break') - permission_required = 'roles.sales_order.change' - - -class PartInternalPriceBreakDelete(PartSalePriceBreakDelete): - """ View for deleting a internal price break """ - - model = PartInternalPriceBreak - ajax_form_title = _("Delete Internal Price Break") - permission_required = 'roles.sales_order.delete' diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 35d5d0d5a6..d552bcb9d7 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -1930,7 +1930,9 @@ function loadPriceBreakTable(table, options) { formatNoMatches: function() { return `{% trans "No ${human_name} information found" %}`; }, - queryParams: {part: options.part}, + queryParams: { + part: options.part + }, url: options.url, onLoadSuccess: function(tableData) { if (linkedGraph) { @@ -2036,36 +2038,45 @@ function initPriceBreakSet(table, options) { } pb_new_btn.click(function() { - launchModalForm(pb_new_url, - { - success: reloadPriceBreakTable, - data: { - part: part_id, - } - } - ); + + constructForm(pb_new_url, { + fields: { + part: { + hidden: true, + value: part_id, + }, + quantity: {}, + price: {}, + price_currency: {}, + }, + method: 'POST', + title: '{% trans "Add Price Break" %}', + onSuccess: reloadPriceBreakTable, + }); }); table.on('click', `.button-${pb_url_slug}-delete`, function() { var pk = $(this).attr('pk'); - launchModalForm( - `/part/${pb_url_slug}/${pk}/delete/`, - { - success: reloadPriceBreakTable - } - ); + constructForm(`${pb_url}${pk}/`, { + method: 'DELETE', + title: '{% trans "Delete Price Break" %}', + onSuccess: reloadPriceBreakTable, + }); }); table.on('click', `.button-${pb_url_slug}-edit`, function() { var pk = $(this).attr('pk'); - launchModalForm( - `/part/${pb_url_slug}/${pk}/edit/`, - { - success: reloadPriceBreakTable - } - ); + constructForm(`${pb_url}${pk}/`, { + fields: { + quantity: {}, + price: {}, + price_currency: {}, + }, + title: '{% trans "Edit Price Break" %}', + onSuccess: reloadPriceBreakTable, + }); }); }