From 1614c6e08af3fdbd61e26448b8bc9c39516dea5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 21:51:42 +0200 Subject: [PATCH 01/29] Add in sale price model --- InvenTree/order/forms.py | 1 + .../migrations/0045_auto_20210504_1946.py | 24 +++++++++++++++++++ InvenTree/order/models.py | 10 ++++++++ InvenTree/order/serializers.py | 4 ++++ tasks.py | 2 +- 5 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 InvenTree/order/migrations/0045_auto_20210504_1946.py diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 4c9caf3b5316..8536c71ef54b 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -211,6 +211,7 @@ class Meta: 'part', 'quantity', 'reference', + 'sale_price', 'notes' ] diff --git a/InvenTree/order/migrations/0045_auto_20210504_1946.py b/InvenTree/order/migrations/0045_auto_20210504_1946.py new file mode 100644 index 000000000000..a8d9469dc700 --- /dev/null +++ b/InvenTree/order/migrations/0045_auto_20210504_1946.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2 on 2021-05-04 19:46 + +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0044_auto_20210404_2016'), + ] + + operations = [ + migrations.AddField( + model_name='salesorderlineitem', + name='sale_price', + field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='USD', help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'), + ), + migrations.AddField( + model_name='salesorderlineitem', + name='sale_price_currency', + field=djmoney.models.fields.CurrencyField(choices=[('AUD', 'Australian Dollar'), ('GBP', 'British Pound'), ('CAD', 'Canadian Dollar'), ('EUR', 'Euro'), ('JPY', 'Japanese Yen'), ('NZD', 'New Zealand Dollar'), ('USD', 'US Dollar')], default='USD', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ea70c3b56afd..f0df4a7ff1b6 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -664,12 +664,22 @@ class SalesOrderLineItem(OrderLineItem): Attributes: order: Link to the SalesOrder that this line item belongs to part: Link to a Part object (may be null) + sale_price: The unit sale price for this OrderLineItem """ order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', verbose_name=_('Order'), help_text=_('Sales Order')) part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True}) + sale_price = MoneyField( + max_digits=19, + decimal_places=4, + default_currency='USD', + null=True, blank=True, + verbose_name=_('Sale Price'), + help_text=_('Unit sale price'), + ) + class Meta: unique_together = [ ] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index a04798c3032d..2f4545fc30af 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -278,6 +278,7 @@ def __init__(self, *args, **kwargs): quantity = serializers.FloatField() allocated = serializers.FloatField(source='allocated_quantity', read_only=True) fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True) + sale_price_string = serializers.CharField(source='sale_price', read_only=True) class Meta: model = SalesOrderLineItem @@ -294,6 +295,9 @@ class Meta: 'order_detail', 'part', 'part_detail', + 'sale_price', + 'sale_price_currency', + 'sale_price_string', ] diff --git a/tasks.py b/tasks.py index 3065d9724375..6eed4c488ed6 100644 --- a/tasks.py +++ b/tasks.py @@ -65,7 +65,7 @@ def manage(c, cmd, pty=False): cmd - django command to run """ - c.run('cd {path} && python3 manage.py {cmd}'.format( + c.run('cd "{path}" && python3 manage.py {cmd}'.format( path=managePyDir(), cmd=cmd ), pty=pty) From 294e86cc38ca05e7943a4806c448f13d1f60323f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 21:56:25 +0200 Subject: [PATCH 02/29] Add in sale price model --- InvenTree/order/forms.py | 1 + .../migrations/0045_auto_20210504_1946.py | 24 +++++++++++++++++++ InvenTree/order/models.py | 10 ++++++++ InvenTree/order/serializers.py | 4 ++++ 4 files changed, 39 insertions(+) create mode 100644 InvenTree/order/migrations/0045_auto_20210504_1946.py diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 4c9caf3b5316..8536c71ef54b 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -211,6 +211,7 @@ class Meta: 'part', 'quantity', 'reference', + 'sale_price', 'notes' ] diff --git a/InvenTree/order/migrations/0045_auto_20210504_1946.py b/InvenTree/order/migrations/0045_auto_20210504_1946.py new file mode 100644 index 000000000000..a8d9469dc700 --- /dev/null +++ b/InvenTree/order/migrations/0045_auto_20210504_1946.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2 on 2021-05-04 19:46 + +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0044_auto_20210404_2016'), + ] + + operations = [ + migrations.AddField( + model_name='salesorderlineitem', + name='sale_price', + field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='USD', help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'), + ), + migrations.AddField( + model_name='salesorderlineitem', + name='sale_price_currency', + field=djmoney.models.fields.CurrencyField(choices=[('AUD', 'Australian Dollar'), ('GBP', 'British Pound'), ('CAD', 'Canadian Dollar'), ('EUR', 'Euro'), ('JPY', 'Japanese Yen'), ('NZD', 'New Zealand Dollar'), ('USD', 'US Dollar')], default='USD', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ea70c3b56afd..f0df4a7ff1b6 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -664,12 +664,22 @@ class SalesOrderLineItem(OrderLineItem): Attributes: order: Link to the SalesOrder that this line item belongs to part: Link to a Part object (may be null) + sale_price: The unit sale price for this OrderLineItem """ order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', verbose_name=_('Order'), help_text=_('Sales Order')) part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True}) + sale_price = MoneyField( + max_digits=19, + decimal_places=4, + default_currency='USD', + null=True, blank=True, + verbose_name=_('Sale Price'), + help_text=_('Unit sale price'), + ) + class Meta: unique_together = [ ] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index a04798c3032d..2f4545fc30af 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -278,6 +278,7 @@ def __init__(self, *args, **kwargs): quantity = serializers.FloatField() allocated = serializers.FloatField(source='allocated_quantity', read_only=True) fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True) + sale_price_string = serializers.CharField(source='sale_price', read_only=True) class Meta: model = SalesOrderLineItem @@ -294,6 +295,9 @@ class Meta: 'order_detail', 'part', 'part_detail', + 'sale_price', + 'sale_price_currency', + 'sale_price_string', ] From 7fa235282bb3ff91eddea2e6c8350e6dad649861 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 22:50:04 +0200 Subject: [PATCH 03/29] sale price in ui --- InvenTree/order/templates/order/sales_order_detail.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 392a236931c8..e611ebc9e16a 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -223,6 +223,14 @@ field: 'quantity', title: '{% trans "Quantity" %}', }, + { + sortable: true, + field: 'sale_price', + title: '{% trans "Unit Price" %}', + formatter: function(value, row) { + return row.sale_price_string || row.sale_price; + } + }, { field: 'allocated', {% if order.status == SalesOrderStatus.PENDING %} From 251603b69b81306a1a68083019492ec900457b19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 23:47:21 +0200 Subject: [PATCH 04/29] removing temp fix for invoke error --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 6eed4c488ed6..3065d9724375 100644 --- a/tasks.py +++ b/tasks.py @@ -65,7 +65,7 @@ def manage(c, cmd, pty=False): cmd - django command to run """ - c.run('cd "{path}" && python3 manage.py {cmd}'.format( + c.run('cd {path} && python3 manage.py {cmd}'.format( path=managePyDir(), cmd=cmd ), pty=pty) From ee028ef9254eecd75e8d8f30fa1bd04a0aa50504 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 20:38:27 +0200 Subject: [PATCH 05/29] space cleanup --- InvenTree/part/urls.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index b90b11b5682c..c734b7f6100f 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -30,11 +30,10 @@ ] 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'), url(r'^template/(?P\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), - + url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'), url(r'^(?P\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), url(r'^(?P\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), @@ -49,10 +48,10 @@ url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'), url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'), url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'), - + url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'), url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'), - + url(r'^params/', views.PartDetail.as_view(template_name='part/params.html'), name='part-params'), url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), @@ -70,7 +69,7 @@ url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'), - + url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), # Normal thumbnail with form @@ -104,7 +103,7 @@ url(r'^subcategory/', views.CategoryDetail.as_view(template_name='part/subcategory.html'), name='category-subcategory'), url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'), - + # Anything else url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'), ])) From dc4fb64987591ab451a9fd7a99cf47ac39d8e4bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 20:53:53 +0200 Subject: [PATCH 06/29] add in price modal in table --- .../templates/order/sales_order_detail.html | 17 ++++++++++++++++- InvenTree/order/urls.py | 1 + InvenTree/order/views.py | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index e611ebc9e16a..33487398b1ae 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -296,7 +296,8 @@ if (part.assembly) { html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}'); } - + + html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}'); } html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}'); @@ -396,6 +397,20 @@ }, }); }); + + $(".button-price").click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + "{% url 'line-pricing' %}", + { + submit_text: '{% trans "Calculate price" %}', + data: { + line_item: pk, + }, + } + ); + }); } {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 97903d81c174..746a482c4ae9 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -31,6 +31,7 @@ url(r'^new/', views.PurchaseOrderCreate.as_view(), name='po-create'), url(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'), + url(r'^pricing/', views.LineItemPricing.as_view(), name='line-pricing'), # Display detail view for a single purchase order url(r'^(?P\d+)/', include(purchase_order_detail_urls)), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 284a24fcf56b..9b642feb831d 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -29,6 +29,7 @@ from common.models import InvenTreeSetting from . import forms as order_forms +from part.views import PartPricing from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.helpers import DownloadFile, str2bool @@ -1559,3 +1560,17 @@ class SalesOrderAllocationDelete(AjaxDeleteView): ajax_form_title = _("Remove allocation") context_object_name = 'allocation' ajax_template_name = "order/so_allocation_delete.html" + + +class LineItemPricing(PartPricing): + """ View for inspecting part pricing information """ + + def get_part(self): + if 'line_item' in self.request.GET: + try: + part_id = self.request.GET.get('line_item') + return SalesOrderLineItem.objects.get(id=part_id).part + except Part.DoesNotExist: + return None + else: + return None From b392586a0866a662b9be8a49a2caa4ad77b571f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 20:55:37 +0200 Subject: [PATCH 07/29] quantity also for modal --- .../templates/order/sales_order_detail.html | 3 +++ InvenTree/part/views.py | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 33487398b1ae..d57c0da98a2b 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -400,6 +400,8 @@ $(".button-price").click(function() { var pk = $(this).attr('pk'); + var idx = $(this).closest('tr').attr('data-index'); + var row = table.bootstrapTable('getData')[idx]; launchModalForm( "{% url 'line-pricing' %}", @@ -407,6 +409,7 @@ submit_text: '{% trans "Calculate price" %}', data: { line_item: pk, + quantity: row.quantity, }, } ); diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d7c68dd6a301..b7bebb6d05f0 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1959,8 +1959,19 @@ class PartPricing(AjaxView): def get_quantity(self): """ Return set quantity in decimal format """ - - return Decimal(self.request.POST.get('quantity', 1)) + + # check POST + qty = self.request.POST.get('quantity', None) + + # check GET + if not qty: + qty = self.request.GET.get('quantity', None) + + # nothing found: return 1 + if not qty: + return Decimal(1) + # return as decimal + return Decimal(qty) def get_part(self): try: @@ -2048,7 +2059,8 @@ def get_pricing(self, quantity=1, currency=None): def get(self, request, *args, **kwargs): - return self.renderJsonResponse(request, self.form_class(), context=self.get_pricing()) + quantity = self.get_quantity() + return self.renderJsonResponse(request, self.form_class(initial={'quantity': quantity}), context=self.get_pricing(quantity)) def post(self, request, *args, **kwargs): From 2cfb9c60a3eaf5597d057bcedca85f72823940ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 20:57:00 +0200 Subject: [PATCH 08/29] space cleanup --- InvenTree/InvenTree/static/script/inventree/inventree.js | 2 +- InvenTree/order/templates/order/sales_order_detail.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index 238fc0a6a668..d4269c1ffb19 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -100,7 +100,7 @@ function makeIconButton(icon, cls, pk, title, options={}) { if (options.disabled) { extraProps += "disabled='true' "; } - + html += ``; diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index d57c0da98a2b..09c15eb8656d 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -287,7 +287,7 @@ html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}'); } - html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}'); + html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}'); if (part.purchaseable) { html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}'); From 287a05ddc5cece2f534c5d81dc575d02b5a1935c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 21:48:58 +0200 Subject: [PATCH 09/29] clearer spacing in html --- .../part/templates/part/part_pricing.html | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/InvenTree/part/templates/part/part_pricing.html b/InvenTree/part/templates/part/part_pricing.html index b14be2c61fb5..992281321021 100644 --- a/InvenTree/part/templates/part/part_pricing.html +++ b/InvenTree/part/templates/part/part_pricing.html @@ -19,9 +19,10 @@

{% trans 'Quantity' %}

{{ quantity }} - {% if part.supplier_count > 0 %} + +{% if part.supplier_count > 0 %}

{% trans 'Supplier Pricing' %}

- +
{% if min_total_buy_price %} @@ -42,12 +43,12 @@

{% trans 'Supplier Pricing' %}

{% endif %} -
{% trans 'Unit Cost' %}
- {% endif %} + +{% endif %} - {% if part.bom_count > 0 %} +{% if part.bom_count > 0 %}

{% trans 'BOM Pricing' %}

- +
{% if min_total_bom_price %} @@ -75,8 +76,8 @@

{% trans 'BOM Pricing' %}

{% endif %} -
{% trans 'Unit Cost' %}
- {% endif %} + +{% endif %} {% if min_unit_buy_price or min_unit_bom_price %} {% else %} @@ -84,7 +85,5 @@

{% trans 'BOM Pricing' %}

{% trans 'No pricing information is available for this part.' %} {% endif %} -
- {% endblock %} \ No newline at end of file From 1a227faec4fa3f0d1fe5a77546e56771283dff22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 23:42:52 +0200 Subject: [PATCH 10/29] abstracting get_price --- InvenTree/common/models.py | 68 +++++++++++++++++++++++++++++++++++++ InvenTree/company/models.py | 65 +---------------------------------- 2 files changed, 69 insertions(+), 64 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index bc2ca4214b75..4280177629fe 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -7,6 +7,8 @@ from __future__ import unicode_literals import os +import decimal +import math from django.db import models, transaction from django.db.utils import IntegrityError, OperationalError @@ -730,6 +732,72 @@ def convert_to(self, currency_code): return converted.amount +def get_price(instance, quantity, moq=True, multiples=True, currency=None): + """ Calculate the price based on quantity price breaks. + + - Don't forget to add in flat-fee cost (base_cost field) + - If MOQ (minimum order quantity) is required, bump quantity + - If order multiples are to be observed, then we need to calculate based on that, too + """ + + price_breaks = instance.price_breaks.all() + + # No price break information available? + if len(price_breaks) == 0: + return None + + # Check if quantity is fraction and disable multiples + multiples = (quantity % 1 == 0) + + # Order multiples + if multiples: + quantity = int(math.ceil(quantity / instance.multiple) * instance.multiple) + + pb_found = False + pb_quantity = -1 + pb_cost = 0.0 + + if currency is None: + # Default currency selection + currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + + pb_min = None + for pb in instance.price_breaks.all(): + # Store smallest price break + if not pb_min: + pb_min = pb + + # Ignore this pricebreak (quantity is too high) + if pb.quantity > quantity: + continue + + pb_found = True + + # If this price-break quantity is the largest so far, use it! + if pb.quantity > pb_quantity: + pb_quantity = pb.quantity + + # Convert everything to the selected currency + pb_cost = pb.convert_to(currency) + + # Use smallest price break + if not pb_found and pb_min: + # Update price break information + pb_quantity = pb_min.quantity + pb_cost = pb_min.convert_to(currency) + # Trigger cost calculation using smallest price break + pb_found = True + + # Convert quantity to decimal.Decimal format + quantity = decimal.Decimal(f'{quantity}') + + if pb_found: + cost = pb_cost * quantity + return InvenTree.helpers.normalize(cost + instance.base_cost) + else: + return None + + class ColorTheme(models.Model): """ Color Theme Setting """ diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 89a3f6c9bfe1..baac95d44d43 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -558,70 +558,7 @@ def add_price_break(self, quantity, price): price=price ) - def get_price(self, quantity, moq=True, multiples=True, currency=None): - """ Calculate the supplier price based on quantity price breaks. - - - Don't forget to add in flat-fee cost (base_cost field) - - If MOQ (minimum order quantity) is required, bump quantity - - If order multiples are to be observed, then we need to calculate based on that, too - """ - - price_breaks = self.price_breaks.all() - - # No price break information available? - if len(price_breaks) == 0: - return None - - # Check if quantity is fraction and disable multiples - multiples = (quantity % 1 == 0) - - # Order multiples - if multiples: - quantity = int(math.ceil(quantity / self.multiple) * self.multiple) - - pb_found = False - pb_quantity = -1 - pb_cost = 0.0 - - if currency is None: - # Default currency selection - currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') - - pb_min = None - for pb in self.price_breaks.all(): - # Store smallest price break - if not pb_min: - pb_min = pb - - # Ignore this pricebreak (quantity is too high) - if pb.quantity > quantity: - continue - - pb_found = True - - # If this price-break quantity is the largest so far, use it! - if pb.quantity > pb_quantity: - pb_quantity = pb.quantity - - # Convert everything to the selected currency - pb_cost = pb.convert_to(currency) - - # Use smallest price break - if not pb_found and pb_min: - # Update price break information - pb_quantity = pb_min.quantity - pb_cost = pb_min.convert_to(currency) - # Trigger cost calculation using smallest price break - pb_found = True - - # Convert quantity to decimal.Decimal format - quantity = decimal.Decimal(f'{quantity}') - - if pb_found: - cost = pb_cost * quantity - return normalize(cost + self.base_cost) - else: - return None + get_price = common.models.get_price def open_orders(self): """ Return a database query for PO line items for this SupplierPart, From 1b7ade94052178b04cc7a5c8bbbb410a8e94620f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 23:47:46 +0200 Subject: [PATCH 11/29] adding in missing parts for full saleprice --- .../migrations/0065_auto_20210505_2144.py | 24 ++++++++++++ InvenTree/part/models.py | 38 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 InvenTree/part/migrations/0065_auto_20210505_2144.py diff --git a/InvenTree/part/migrations/0065_auto_20210505_2144.py b/InvenTree/part/migrations/0065_auto_20210505_2144.py new file mode 100644 index 000000000000..328ce1f588ed --- /dev/null +++ b/InvenTree/part/migrations/0065_auto_20210505_2144.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2 on 2021-05-05 21:44 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0064_auto_20210404_2016'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='base_cost', + field=models.DecimalField(decimal_places=3, default=0, help_text='Minimum charge (e.g. stocking fee)', max_digits=10, validators=[django.core.validators.MinValueValidator(0)], verbose_name='base cost'), + ), + migrations.AddField( + model_name='part', + name='multiple', + field=models.PositiveIntegerField(default=1, help_text='Sell multiple', validators=[django.core.validators.MinValueValidator(1)], verbose_name='multiple'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 137781ba2b3c..4c7086f51d85 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1611,6 +1611,44 @@ def get_price_range(self, quantity=1, buy=True, bom=True): max(buy_price_range[1], bom_price_range[1]) ) + base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], verbose_name=_('base cost'), help_text=_('Minimum charge (e.g. stocking fee)')) + + multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name=_('multiple'), help_text=_('Sell multiple')) + + get_price = common.models.get_price + + @property + def has_price_breaks(self): + return self.price_breaks.count() > 0 + + @property + def price_breaks(self): + """ Return the associated price breaks in the correct order """ + return self.salepricebreaks.order_by('quantity').all() + + @property + def unit_pricing(self): + return self.get_price(1) + + def add_price_break(self, quantity, price): + """ + Create a new price break for this part + + args: + quantity - Numerical quantity + price - Must be a Money object + """ + + # Check if a price break at that quantity already exists... + if self.price_breaks.filter(quantity=quantity, part=self.pk).exists(): + return + + PartSellPriceBreak.objects.create( + part=self, + quantity=quantity, + price=price + ) + @transaction.atomic def copy_bom_from(self, other, clear=True, **kwargs): """ From 030865f8dd96b45f621c80b5833301786b004c35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 23:49:04 +0200 Subject: [PATCH 12/29] sale price in pricing table --- InvenTree/part/templates/part/part_pricing.html | 14 ++++++++++++++ InvenTree/part/views.py | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/InvenTree/part/templates/part/part_pricing.html b/InvenTree/part/templates/part/part_pricing.html index 992281321021..df43ed879933 100644 --- a/InvenTree/part/templates/part/part_pricing.html +++ b/InvenTree/part/templates/part/part_pricing.html @@ -79,6 +79,20 @@

{% trans 'BOM Pricing' %}

{% endif %} +{% if total_part_price %} +

{% trans 'Sale Price' %}

+ + + + + + + + + +
{% trans 'Unit Cost' %}{% include "price.html" with price=unit_part_price %}
{% trans 'Total Cost' %}{% include "price.html" with price=total_part_price %}
+{% endif %} + {% if min_unit_buy_price or min_unit_bom_price %} {% else %}
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index b7bebb6d05f0..5af90cb3838b 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2055,6 +2055,12 @@ def get_pricing(self, quantity=1, currency=None): ctx['max_total_bom_price'] = max_bom_price ctx['max_unit_bom_price'] = max_unit_bom_price + # part pricing information + part_price = part.get_price(quantity) + if part_price is not None: + ctx['total_part_price'] = round(part_price, 3) + ctx['unit_part_price'] = round(part_price / quantity, 3) + return ctx def get(self, request, *args, **kwargs): From efa9da2ce1b38a98afb09f3b7ab2a965529a8432 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 00:00:13 +0200 Subject: [PATCH 13/29] removed unused imports --- InvenTree/company/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index baac95d44d43..32f1d07a337e 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -6,8 +6,6 @@ from __future__ import unicode_literals import os -import decimal -import math from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator @@ -26,7 +24,6 @@ from stdimage.models import StdImageField from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail -from InvenTree.helpers import normalize from InvenTree.fields import InvenTreeURLField from InvenTree.status_codes import PurchaseOrderStatus From 66f198baa93150a8e1602e4bb6c8836eb20d71df Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 00:17:46 +0200 Subject: [PATCH 14/29] removing duplicate information in pricing table --- InvenTree/part/templates/part/part_pricing.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/InvenTree/part/templates/part/part_pricing.html b/InvenTree/part/templates/part/part_pricing.html index df43ed879933..af916a43fd11 100644 --- a/InvenTree/part/templates/part/part_pricing.html +++ b/InvenTree/part/templates/part/part_pricing.html @@ -4,11 +4,6 @@ {% block pre_form_content %} -
-{% blocktrans %}Pricing information for:
{{part}}.{% endblocktrans %} -
- -

{% trans 'Quantity' %}

From c2a5e1fd23ae4f70661cba2ebdea7eca3a26c0c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 16:29:03 +0200 Subject: [PATCH 15/29] moved the special stuff into the inherited view --- InvenTree/order/views.py | 8 ++++++++ InvenTree/part/views.py | 16 ++-------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 2ed7e76c4998..e68ff91e83e2 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1586,3 +1586,11 @@ def get_part(self): return None else: return None + + def get_quantity(self): + """ Return set quantity in decimal format """ + qty = Decimal(self.request.GET.get('quantity', 1)) + if qty == 1: + return Decimal(self.request.POST.get('quantity', 1)) + return qty + diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 18bbd5b36120..be33d07ee2f1 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1956,22 +1956,10 @@ class PartPricing(AjaxView): form_class = part_forms.PartPriceForm role_required = ['sales_order.view', 'part.view'] - + def get_quantity(self): """ Return set quantity in decimal format """ - - # check POST - qty = self.request.POST.get('quantity', None) - - # check GET - if not qty: - qty = self.request.GET.get('quantity', None) - - # nothing found: return 1 - if not qty: - return Decimal(1) - # return as decimal - return Decimal(qty) + return Decimal(self.request.POST.get('quantity', 1)) def get_part(self): try: From 4830ff28bf88093607d7d8fe953669855c0f6bc0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 16:34:37 +0200 Subject: [PATCH 16/29] new function for initials --- InvenTree/part/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index be33d07ee2f1..d157f7cf36d3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2051,6 +2051,10 @@ def get_pricing(self, quantity=1, currency=None): return ctx + def get_initials(self): + """ returns initials for form """ + return {'quantity': self.get_quantity()} + def get(self, request, *args, **kwargs): quantity = self.get_quantity() @@ -2063,8 +2067,7 @@ def post(self, request, *args, **kwargs): quantity = self.get_quantity() # Retain quantity value set by user - form = self.form_class() - form.fields['quantity'].initial = quantity + form = self.form_class(initial=self.get_initials()) # TODO - How to handle pricing in different currencies? currency = None From 90c207b935db14cd93c446bcd507eb44cf8b2368 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 16:45:39 +0200 Subject: [PATCH 17/29] keeping part id in inherited form --- InvenTree/order/views.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index e68ff91e83e2..37b758eb092b 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1577,6 +1577,11 @@ class SalesOrderAllocationDelete(AjaxDeleteView): class LineItemPricing(PartPricing): """ View for inspecting part pricing information """ + class EnhancedForm(PartPricing.form_class): + pk = IntegerField(widget = HiddenInput()) + + form_class = EnhancedForm + def get_part(self): if 'line_item' in self.request.GET: try: @@ -1584,6 +1589,12 @@ def get_part(self): return SalesOrderLineItem.objects.get(id=part_id).part except Part.DoesNotExist: return None + elif 'pk' in self.request.POST: + try: + part_id = self.request.POST.get('pk') + return Part.objects.get(id=part_id) + except Part.DoesNotExist: + return None else: return None @@ -1594,3 +1605,7 @@ def get_quantity(self): return Decimal(self.request.POST.get('quantity', 1)) return qty + def get_initials(self): + initials = super().get_initials() + initials['pk'] = self.get_part().id + return initials From 660a3f9410af502ebc51e723770364f08f883e94 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 16:46:29 +0200 Subject: [PATCH 18/29] cleaner get function --- InvenTree/part/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d157f7cf36d3..d946e6f509f7 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2056,9 +2056,9 @@ def get_initials(self): return {'quantity': self.get_quantity()} def get(self, request, *args, **kwargs): - - quantity = self.get_quantity() - return self.renderJsonResponse(request, self.form_class(initial={'quantity': quantity}), context=self.get_pricing(quantity)) + init = self.get_initials() + qty = self.get_quantity() + return self.renderJsonResponse(request, self.form_class(initial=init), context=self.get_pricing(qty)) def post(self, request, *args, **kwargs): From 792b2d11c0a926d68304c9bf224767d93de66260 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 16:46:52 +0200 Subject: [PATCH 19/29] cleanup --- InvenTree/part/views.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d946e6f509f7..040f32d44108 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1968,12 +1968,7 @@ def get_part(self): return None def get_pricing(self, quantity=1, currency=None): - - # try: - # quantity = int(quantity) - # except ValueError: - # quantity = 1 - + """ returns context with pricing information """ if quantity <= 0: quantity = 1 From aac05db6bf87410875daa5fc42ef66674e6eabfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 17:15:10 +0200 Subject: [PATCH 20/29] style fixing --- InvenTree/order/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 37b758eb092b..d9c28457d284 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -12,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from django.views.generic import DetailView, ListView, UpdateView from django.views.generic.edit import FormMixin -from django.forms import HiddenInput +from django.forms import HiddenInput, IntegerField import logging from decimal import Decimal, InvalidOperation @@ -1578,7 +1578,7 @@ class LineItemPricing(PartPricing): """ View for inspecting part pricing information """ class EnhancedForm(PartPricing.form_class): - pk = IntegerField(widget = HiddenInput()) + pk = IntegerField(widget=HiddenInput()) form_class = EnhancedForm From 053793288b53a37413d1fbd8b1062c0e49a09560 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 18:05:43 +0200 Subject: [PATCH 21/29] same spacing for tables thanks @eeintech --- InvenTree/InvenTree/static/css/inventree.css | 18 ++++++++++++++++++ .../part/templates/part/part_pricing.html | 16 ++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 9d322f339d87..e7b8aeb71e62 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -466,6 +466,24 @@ background: #eee; } +/* pricing table widths */ +.table-price-two tr td:first-child { + width: 40%; +} + +.table-price-three tr td:first-child { + width: 40%; +} + +.table-price-two tr td:last-child { + width: 60%; +} + +.table-price-three tr td:last-child { + width: 30%; +} +/* !pricing table widths */ + .btn-glyph { padding-left: 6px; padding-right: 6px; diff --git a/InvenTree/part/templates/part/part_pricing.html b/InvenTree/part/templates/part/part_pricing.html index af916a43fd11..30628b5fc2c9 100644 --- a/InvenTree/part/templates/part/part_pricing.html +++ b/InvenTree/part/templates/part/part_pricing.html @@ -4,20 +4,20 @@ {% block pre_form_content %} -
{% trans 'Part' %}
+
- + - +
{% trans 'Part' %}{{ part }}{{ part }}
{% trans 'Quantity' %}{{ quantity }}{{ quantity }}
{% if part.supplier_count > 0 %}

{% trans 'Supplier Pricing' %}

- +
{% if min_total_buy_price %} @@ -43,7 +43,7 @@

{% trans 'Supplier Pricing' %}

{% if part.bom_count > 0 %}

{% trans 'BOM Pricing' %}

-
{% trans 'Unit Cost' %}
+
{% if min_total_bom_price %} @@ -76,14 +76,14 @@

{% trans 'BOM Pricing' %}

{% if total_part_price %}

{% trans 'Sale Price' %}

-
{% trans 'Unit Cost' %}
+
- + - +
{% trans 'Unit Cost' %}{% include "price.html" with price=unit_part_price %}{% include "price.html" with price=unit_part_price %}
{% trans 'Total Cost' %}{% include "price.html" with price=total_part_price %}{% include "price.html" with price=total_part_price %}
{% endif %} From 985967fccb05aeab28aa3ec1e25c582aade5fd57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 07:13:23 +0200 Subject: [PATCH 22/29] save return of part.id --- InvenTree/order/views.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d9c28457d284..40f57d247c19 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1582,22 +1582,26 @@ class EnhancedForm(PartPricing.form_class): form_class = EnhancedForm - def get_part(self): + def get_part(self, id=False): if 'line_item' in self.request.GET: try: part_id = self.request.GET.get('line_item') - return SalesOrderLineItem.objects.get(id=part_id).part + part = SalesOrderLineItem.objects.get(id=part_id).part except Part.DoesNotExist: return None elif 'pk' in self.request.POST: try: part_id = self.request.POST.get('pk') - return Part.objects.get(id=part_id) + part = Part.objects.get(id=part_id) except Part.DoesNotExist: return None else: return None + if id: + return part.id + return part + def get_quantity(self): """ Return set quantity in decimal format """ qty = Decimal(self.request.GET.get('quantity', 1)) @@ -1607,5 +1611,6 @@ def get_quantity(self): def get_initials(self): initials = super().get_initials() - initials['pk'] = self.get_part().id + initials['pk'] = self.get_part(id=True) + return initials From 09fe9ccf1161156e44088545260b72cbcce0374a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 07:15:33 +0200 Subject: [PATCH 23/29] sales order item tracking --- InvenTree/order/views.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 40f57d247c19..81c2082c1c41 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1579,6 +1579,7 @@ class LineItemPricing(PartPricing): class EnhancedForm(PartPricing.form_class): pk = IntegerField(widget=HiddenInput()) + so_line = IntegerField(widget=HiddenInput()) form_class = EnhancedForm @@ -1602,6 +1603,21 @@ def get_part(self, id=False): return part.id return part + def get_so(self, pk=False): + so_line = self.request.GET.get('line_item', None) + if not so_line: + so_line = self.request.POST.get('so_line', None) + + if so_line: + try: + sales_order = SalesOrderLineItem.objects.get(pk=so_line) + if pk: + return sales_order.pk + return sales_order + except Part.DoesNotExist: + return None + return None + def get_quantity(self): """ Return set quantity in decimal format """ qty = Decimal(self.request.GET.get('quantity', 1)) @@ -1612,5 +1628,8 @@ def get_quantity(self): def get_initials(self): initials = super().get_initials() initials['pk'] = self.get_part(id=True) + initials['so_line'] = self.get_so(pk=True) + return initials + return initials From f73863ea517dd74c6de5ee38ac7c1a2e093cd661 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 07:18:13 +0200 Subject: [PATCH 24/29] adding in cstm action buttons function --- InvenTree/templates/js/modals.js | 35 ++++++++++++++++++++++++++++++++ InvenTree/templates/modals.html | 4 ++++ 2 files changed, 39 insertions(+) diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index 004e81c0007c..8d34d790d830 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -377,6 +377,15 @@ function modalSubmit(modal, callback) { $(modal).on('click', '#modal-form-submit', function() { callback(); }); + + $(modal).on('click', '.modal-form-button', function() { + // Append data to form + var name = $(this).attr('name'); + var value = $(this).attr('value'); + var input = ''; + $('.js-modal-form').append(input); + callback(); + }); } @@ -659,6 +668,25 @@ function attachSecondaries(modal, secondaries) { } } +function insertActionButton(modal, options) { + /* Insert a custom submition button */ + + var html = ""; + html += ""; + html += ""; + + $(modal).find('#modal-footer-buttons').append(html); +} + +function attachButtons(modal, buttons) { + /* Attach a provided list of buttons */ + + for (var i = 0; i < buttons.length; i++) { + insertActionButton(modal, buttons[i]); + } +} + function attachFieldCallback(modal, callback) { /* Attach a 'callback' function to a given field in the modal form. @@ -808,6 +836,9 @@ function launchModalForm(url, options = {}) { var submit_text = options.submit_text || '{% trans "Submit" %}'; var close_text = options.close_text || '{% trans "Close" %}'; + // Clean custom action buttons + $(modal).find('#modal-footer-buttons').html(''); + // Form the ajax request to retrieve the django form data ajax_data = { url: url, @@ -852,6 +883,10 @@ function launchModalForm(url, options = {}) { handleModalForm(url, options); } + if (options.buttons) { + attachButtons(modal, options.buttons); + } + } else { $(modal).modal('hide'); showAlertDialog('{% trans "Invalid server response" %}', '{% trans "JSON response missing form data" %}'); diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html index 9850f482c56c..e394b2831445 100644 --- a/InvenTree/templates/modals.html +++ b/InvenTree/templates/modals.html @@ -25,6 +25,7 @@
@@ -49,6 +50,7 @@ @@ -69,6 +71,7 @@ @@ -90,6 +93,7 @@ From c775c4611fc04b95ca4984acd6cd7a52584cfe34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 07:19:43 +0200 Subject: [PATCH 25/29] adding custom action button save the changes to the db and return success-json --- .../templates/order/sales_order_detail.html | 3 ++ InvenTree/order/views.py | 42 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 09c15eb8656d..e4a399a0e135 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -411,6 +411,9 @@ line_item: pk, quantity: row.quantity, }, + buttons: [{name: 'update_price', + title: '{% trans "Update Unit Price" %}'},], + success: reloadTable, } ); }); diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 81c2082c1c41..5a1be6e5be15 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals from django.db import transaction +from django.http.response import JsonResponse from django.shortcuts import get_object_or_404 from django.core.exceptions import ValidationError from django.urls import reverse @@ -1631,5 +1632,44 @@ def get_initials(self): initials['so_line'] = self.get_so(pk=True) return initials + def post(self, request, *args, **kwargs): + response = None + # parse extra actions + REF = 'act-btn_' + act_btn = [a.replace(REF, '') for a in self.request.POST if REF in a] + + # check if extra action was passed + if act_btn and act_btn[0] == 'update_price': + # get sales order + so_line = self.get_so() + if not so_line: + self.data = {'non_field_errors':[_('Sales order not found')]} + else: + quantity = self.get_quantity() + price = self.get_pricing(quantity).get('unit_part_price', None) + + if not price: + self.data = {'non_field_errors':[_('Price not found')]} + else: + # set normal update note + note = _('Updated {part} unit-price to {price}') + + # check qunatity and update if different + if so_line.quantity != quantity: + so_line.quantity = quantity + note = _('Updated {part} unit-price to {price} and quantity to {qty}') + + # update sale_price + so_line.sale_price = price + so_line.save() + + # parse response + data = { + 'form_valid': True, + 'success': note.format(part=str(so_line.part), price=str(so_line.sale_price), qty=quantity) + } + return JsonResponse(data=data) + + # let the normal pricing view run + return super().post(request, *args, **kwargs) - return initials From ae01503a9e8814ef101f6955c3409b24f0fecedc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 07:20:43 +0200 Subject: [PATCH 26/29] handeling data in an inheritable way --- InvenTree/part/views.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 040f32d44108..710b434c82de 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2067,10 +2067,14 @@ def post(self, request, *args, **kwargs): # TODO - How to handle pricing in different currencies? currency = None + # check if data is set + try: + data = self.data + except Exception as _e: + data = {} + # Always mark the form as 'invalid' (the user may wish to keep getting pricing data) - data = { - 'form_valid': False, - } + data['form_valid'] = False return self.renderJsonResponse(request, form, data=data, context=self.get_pricing(quantity, currency)) From 9e59d41f125b5249c79d3ccf985252c5c3353d58 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 07:46:35 +0200 Subject: [PATCH 27/29] style improvments --- InvenTree/order/views.py | 8 +++----- InvenTree/part/views.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 5a1be6e5be15..4079080d666a 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1633,23 +1633,22 @@ def get_initials(self): return initials def post(self, request, *args, **kwargs): - response = None # parse extra actions REF = 'act-btn_' - act_btn = [a.replace(REF, '') for a in self.request.POST if REF in a] + act_btn = [a.replace(REF, '') for a in self.request.POST if REF in a] # check if extra action was passed if act_btn and act_btn[0] == 'update_price': # get sales order so_line = self.get_so() if not so_line: - self.data = {'non_field_errors':[_('Sales order not found')]} + self.data = {'non_field_errors': [_('Sales order not found')]} else: quantity = self.get_quantity() price = self.get_pricing(quantity).get('unit_part_price', None) if not price: - self.data = {'non_field_errors':[_('Price not found')]} + self.data = {'non_field_errors': [_('Price not found')]} else: # set normal update note note = _('Updated {part} unit-price to {price}') @@ -1672,4 +1671,3 @@ def post(self, request, *args, **kwargs): # let the normal pricing view run return super().post(request, *args, **kwargs) - diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 710b434c82de..ad98095fb1ba 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2070,7 +2070,7 @@ def post(self, request, *args, **kwargs): # check if data is set try: data = self.data - except Exception as _e: + except AttributeError: data = {} # Always mark the form as 'invalid' (the user may wish to keep getting pricing data) From b6043af7c0f3bd46a261af2eda27b818be4e23ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 15:35:35 +0200 Subject: [PATCH 28/29] auto-set price if sales-order line is added --- InvenTree/order/views.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 4079080d666a..29f70511b6ca 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1247,6 +1247,17 @@ def get_initial(self): return initials + def save(self, form): + ret = form.save() + # check if price s set in form - else autoset + if not ret.sale_price: + price = ret.part.get_price(ret.quantity) + # only if price is avail + if price: + ret.sale_price = price / ret.quantity + ret.save() + self.object = ret + return ret class SOLineItemEdit(AjaxUpdateView): """ View for editing a SalesOrderLineItem """ From 63cf75eefca622c156a1fb4ba5180a30f15985c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 15:37:15 +0200 Subject: [PATCH 29/29] styling again --- InvenTree/order/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 29f70511b6ca..691379963169 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1259,6 +1259,7 @@ def save(self, form): self.object = ret return ret + class SOLineItemEdit(AjaxUpdateView): """ View for editing a SalesOrderLineItem """