From 1614c6e08af3fdbd61e26448b8bc9c39516dea5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 21:51:42 +0200 Subject: [PATCH 01/35] 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 4c9caf3b53..8536c71ef5 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -211,6 +211,7 @@ class EditSalesOrderLineItemForm(HelperForm): '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 0000000000..a8d9469dc7 --- /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 ea70c3b56a..f0df4a7ff1 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 a04798c303..2f4545fc30 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -278,6 +278,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): 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 SOLineItemSerializer(InvenTreeModelSerializer): 'order_detail', 'part', 'part_detail', + 'sale_price', + 'sale_price_currency', + 'sale_price_string', ] diff --git a/tasks.py b/tasks.py index 3065d97243..6eed4c488e 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/35] 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 4c9caf3b53..8536c71ef5 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -211,6 +211,7 @@ class EditSalesOrderLineItemForm(HelperForm): '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 0000000000..a8d9469dc7 --- /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 ea70c3b56a..f0df4a7ff1 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 a04798c303..2f4545fc30 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -278,6 +278,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): 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 SOLineItemSerializer(InvenTreeModelSerializer): '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/35] 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 392a236931..e611ebc9e1 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -223,6 +223,14 @@ $("#so-lines-table").inventreeTable({ 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/35] 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 6eed4c488e..3065d97243 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/35] 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 b90b11b568..c734b7f610 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -30,11 +30,10 @@ sale_price_break_urls = [ ] 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 @@ part_detail_urls = [ 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 @@ part_detail_urls = [ 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 @@ category_urls = [ 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/35] 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 e611ebc9e1..33487398b1 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -296,7 +296,8 @@ $("#so-lines-table").inventreeTable({ 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 @@ function setupCallbacks() { }, }); }); + + $(".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 97903d81c1..746a482c4a 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -31,6 +31,7 @@ purchase_order_urls = [ 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 284a24fcf5..9b642feb83 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -29,6 +29,7 @@ from part.models import Part 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/35] 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 33487398b1..d57c0da98a 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -400,6 +400,8 @@ function setupCallbacks() { $(".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 @@ function setupCallbacks() { 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 d7c68dd6a3..b7bebb6d05 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 @@ class PartPricing(AjaxView): 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/35] 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 238fc0a6a6..d4269c1ffb 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 d57c0da98a..09c15eb865 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -287,7 +287,7 @@ $("#so-lines-table").inventreeTable({ 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/35] 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 b14be2c61f..9922813210 100644 --- a/InvenTree/part/templates/part/part_pricing.html +++ b/InvenTree/part/templates/part/part_pricing.html @@ -19,9 +19,10 @@ {{ quantity }} - {% if part.supplier_count > 0 %} + +{% if part.supplier_count > 0 %}

{% trans 'Supplier Pricing' %}

- +
{% if min_total_buy_price %} @@ -42,12 +43,12 @@ {% 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 @@ {% endif %} -
{% trans 'Unit Cost' %}
- {% endif %} + +{% endif %} {% if min_unit_buy_price or min_unit_bom_price %} {% else %} @@ -84,7 +85,5 @@ {% 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/35] 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 bc2ca4214b..4280177629 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -7,6 +7,8 @@ These models are 'generic' and do not fit a particular business logic object. 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 @@ class PriceBreak(models.Model): 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 89a3f6c9bf..baac95d44d 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -558,70 +558,7 @@ class SupplierPart(models.Model): 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/35] 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 0000000000..328ce1f588 --- /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 137781ba2b..4c7086f51d 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1611,6 +1611,44 @@ class Part(MPTTModel): 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/35] 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 9922813210..df43ed8799 100644 --- a/InvenTree/part/templates/part/part_pricing.html +++ b/InvenTree/part/templates/part/part_pricing.html @@ -79,6 +79,20 @@ {% 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 b7bebb6d05..5af90cb383 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2055,6 +2055,12 @@ class PartPricing(AjaxView): 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/35] 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 baac95d44d..32f1d07a33 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -6,8 +6,6 @@ Company database model definitions 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 markdownx.models import MarkdownxField 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/35] 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 df43ed8799..af916a43fd 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 f9463fa277bf0d7da0284ed843849e83a560f10b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 00:33:34 +0200 Subject: [PATCH 15/35] fixes navbar-layout shift with narrow dollar-sign --- InvenTree/part/templates/part/navbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html index d8750a49a5..df49841e31 100644 --- a/InvenTree/part/templates/part/navbar.html +++ b/InvenTree/part/templates/part/navbar.html @@ -91,7 +91,7 @@ {% if part.salable and roles.sales_order.view %}
  • - + {% trans "Sale Price" %}
  • From 1259374822ae330ae54bcb7c6745f2ecd2cb69db Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 00:37:45 +0200 Subject: [PATCH 16/35] removes layout-shift due to typo in menubar --- InvenTree/part/templates/part/sale_prices.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/sale_prices.html b/InvenTree/part/templates/part/sale_prices.html index ce50c1f20d..f19d65a59a 100644 --- a/InvenTree/part/templates/part/sale_prices.html +++ b/InvenTree/part/templates/part/sale_prices.html @@ -2,7 +2,7 @@ {% load static %} {% load i18n %} -{% block menubar %}} +{% block menubar %} {% include 'part/navbar.html' with tab='sales-prices' %} {% endblock %} From 39d4129157e9af02ba36ece6b1ddc065a7fbe6cd Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Thu, 6 May 2021 12:29:58 +0200 Subject: [PATCH 17/35] chery picked all fixed trans str from #1438 (#1554) --- InvenTree/InvenTree/validators.py | 6 ++--- InvenTree/company/views.py | 2 +- InvenTree/order/models.py | 2 +- InvenTree/order/views.py | 6 ++--- InvenTree/part/views.py | 2 +- InvenTree/stock/models.py | 38 ++++++++++++++----------------- 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index f8199ef20b..d5fcb8822e 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -74,7 +74,7 @@ def validate_build_order_reference(value): match = re.search(pattern, value) if match is None: - raise ValidationError(_('Reference must match pattern') + f" '{pattern}'") + raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern)) def validate_purchase_order_reference(value): @@ -88,7 +88,7 @@ def validate_purchase_order_reference(value): match = re.search(pattern, value) if match is None: - raise ValidationError(_('Reference must match pattern') + f" '{pattern}'") + raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern)) def validate_sales_order_reference(value): @@ -102,7 +102,7 @@ def validate_sales_order_reference(value): match = re.search(pattern, value) if match is None: - raise ValidationError(_('Reference must match pattern') + f" '{pattern}'") + raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern)) def validate_tree_name(value): diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index be7d326c36..7ba8e5e6bf 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -202,7 +202,7 @@ class CompanyImageDownloadFromURL(AjaxUpdateView): # Check for valid response code if not response.status_code == 200: - form.add_error('url', f"{_('Invalid response')}: {response.status_code}") + form.add_error('url', _('Invalid response: {code}').format(code=response.status_code)) return response.raw.decode_content = True diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index d3b09dec1e..292a5ed492 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -367,7 +367,7 @@ class PurchaseOrder(Order): stock.save() text = _("Received items") - note = f"{_('Received')} {quantity} {_('items against order')} {str(self)}" + note = _('Received {n} items against order {name}').format(n=quantity, name=str(self)) # Add a new transaction note to the newly created stock item stock.addTransactionNote(text, user, note) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index c62a3816d5..0ffff4e340 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1407,7 +1407,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): except StockItem.DoesNotExist: self.form.add_error( 'serials', - _('No matching item for serial') + f" '{serial}'" + _('No matching item for serial {serial}').format(serial=serial) ) continue @@ -1417,7 +1417,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): if not stock_item.in_stock: self.form.add_error( 'serials', - f"'{serial}' " + _("is not in stock") + _('{serial} is not in stock').format(serial=serial) ) continue @@ -1425,7 +1425,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): if stock_item.is_allocated(): self.form.add_error( 'serials', - f"'{serial}' " + _("already allocated to an order") + _('{serial} already allocated to an order').format(serial=serial) ) continue diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d7c68dd6a3..2cf86945d3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -884,7 +884,7 @@ class PartImageDownloadFromURL(AjaxUpdateView): # Check for valid response code if not response.status_code == 200: - form.add_error('url', f"{_('Invalid response')}: {response.status_code}") + form.add_error('url', _('Invalid response: {code}').format(code=response.status_code)) return response.raw.decode_content = True diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7d9520a544..7f22b7a4ef 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -198,7 +198,7 @@ class StockItem(MPTTModel): if add_note: - note = f"{_('Created new stock item for')} {str(self.part)}" + note = _('Created new stock item for {part}').format(part=str(self.part)) # This StockItem is being saved for the first time self.addTransactionNote( @@ -613,7 +613,7 @@ class StockItem(MPTTModel): item.addTransactionNote( _("Assigned to Customer"), user, - notes=_("Manually assigned to customer") + " " + customer.name, + notes=_("Manually assigned to customer {name}").format(name=customer.name), system=True ) @@ -626,9 +626,9 @@ class StockItem(MPTTModel): """ self.addTransactionNote( - _("Returned from customer") + f" {self.customer.name}", + _("Returned from customer {name}").format(name=self.customer.name), user, - notes=_("Returned to location") + f" {location.name}", + notes=_("Returned to location {loc}").format(loc=location.name), system=True ) @@ -789,7 +789,7 @@ class StockItem(MPTTModel): # Add a transaction note to the other item stock_item.addTransactionNote( - _('Installed into stock item') + ' ' + str(self.pk), + _('Installed into stock item {pk}').format(str(self.pk)), user, notes=notes, url=self.get_absolute_url() @@ -797,7 +797,7 @@ class StockItem(MPTTModel): # Add a transaction note to this item self.addTransactionNote( - _('Installed stock item') + ' ' + str(stock_item.pk), + _('Installed stock item {pk}').format(str(stock_item.pk)), user, notes=notes, url=stock_item.get_absolute_url() ) @@ -821,7 +821,7 @@ class StockItem(MPTTModel): # Add a transaction note to the parent item self.belongs_to.addTransactionNote( - _("Uninstalled stock item") + ' ' + str(self.pk), + _("Uninstalled stock item {pk}").format(pk=str(self.pk)), user, notes=notes, url=self.get_absolute_url(), @@ -840,7 +840,7 @@ class StockItem(MPTTModel): # Add a transaction note! self.addTransactionNote( - _('Uninstalled into location') + ' ' + str(location), + _('Uninstalled into location {loc}').formaT(loc=str(location)), user, notes=notes, url=url @@ -966,7 +966,7 @@ class StockItem(MPTTModel): if len(existing) > 0: exists = ','.join([str(x) for x in existing]) - raise ValidationError({"serial_numbers": _("Serial numbers already exist") + ': ' + exists}) + raise ValidationError({"serial_numbers": _("Serial numbers already exist: {exists}").format(exists=exists)}) # Create a new stock item for each unique serial number for serial in serials: @@ -1074,7 +1074,7 @@ class StockItem(MPTTModel): new_stock.addTransactionNote( _("Split from existing stock"), user, - f"{_('Split')} {helpers.normalize(quantity)} {_('items')}" + _('Split {n} items').format(n=helpers.normalize(quantity)) ) # Remove the specified quantity from THIS stock item @@ -1131,10 +1131,10 @@ class StockItem(MPTTModel): return True - msg = f"{_('Moved to')} {str(location)}" - if self.location: - msg += f" ({_('from')} {str(self.location)})" + msg = _("Moved to {loc_new} (from {loc_old})").format(loc_new=str(location), loc_old=str(self.location)) + else: + msg = _('Moved to {loc_new}').format(loc_new=str(location)) self.location = location @@ -1202,9 +1202,7 @@ class StockItem(MPTTModel): if self.updateQuantity(count): - n = helpers.normalize(count) - - text = f"{_('Counted')} {n} {_('items')}" + text = _('Counted {n} items').format(n=helpers.normalize(count)) self.addTransactionNote( text, @@ -1236,9 +1234,8 @@ class StockItem(MPTTModel): return False if self.updateQuantity(self.quantity + quantity): - - n = helpers.normalize(quantity) - text = f"{_('Added')} {n} {_('items')}" + + text = _('Added {n} items').format(n=helpers.normalize(quantity)) self.addTransactionNote( text, @@ -1268,8 +1265,7 @@ class StockItem(MPTTModel): if self.updateQuantity(self.quantity - quantity): - q = helpers.normalize(quantity) - text = f"{_('Removed')} {q} {_('items')}" + text = _('Removed {n1} items').format(n1=helpers.normalize(quantity)) self.addTransactionNote(text, user, From 3a1c233bff72f37b11ede07af3e0d724f94afedf Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Thu, 6 May 2021 16:11:08 +0200 Subject: [PATCH 18/35] Bug/calender (#1558) * fixes calendar not rendering when toggling view * same for po * and builds --- InvenTree/build/templates/build/index.html | 2 ++ InvenTree/order/templates/order/purchase_orders.html | 2 ++ InvenTree/order/templates/order/sales_orders.html | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index fbed17bfa3..0ff4b78ab8 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -158,6 +158,8 @@ $('#view-calendar').click(function() { $("#build-order-calendar").show(); $("#view-list").show(); + + calendar.render(); }); $("#view-list").click(function() { diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 9d15449abf..2d3180a026 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -146,6 +146,8 @@ $('#view-calendar').click(function() { $("#purchase-order-calendar").show(); $("#view-list").show(); + + calendar.render(); }); $("#view-list").click(function() { diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index 8ae538ffce..c6e84f8209 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -141,9 +141,11 @@ $('#view-calendar').click(function() { $(".columns-right").hide(); $(".search").hide(); $('#filter-list-salesorder').hide(); - + $("#sales-order-calendar").show(); $("#view-list").show(); + + calendar.render(); }); $("#view-list").click(function() { From c2a5e1fd23ae4f70661cba2ebdea7eca3a26c0c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 16:29:03 +0200 Subject: [PATCH 19/35] 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 2ed7e76c49..e68ff91e83 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1586,3 +1586,11 @@ class LineItemPricing(PartPricing): 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 18bbd5b361..be33d07ee2 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 20/35] 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 be33d07ee2..d157f7cf36 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2051,6 +2051,10 @@ class PartPricing(AjaxView): 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 @@ class PartPricing(AjaxView): 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 21/35] 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 e68ff91e83..37b758eb09 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 @@ class LineItemPricing(PartPricing): 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 @@ class LineItemPricing(PartPricing): 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 22/35] 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 d157f7cf36..d946e6f509 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2056,9 +2056,9 @@ class PartPricing(AjaxView): 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 23/35] 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 d946e6f509..040f32d441 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1968,12 +1968,7 @@ class PartPricing(AjaxView): 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 24/35] 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 37b758eb09..d9c28457d2 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -12,7 +12,7 @@ from django.urls import reverse 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 25/35] 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 9d322f339d..e7b8aeb71e 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 af916a43fd..30628b5fc2 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 @@ {% if part.bom_count > 0 %}

    {% trans 'BOM Pricing' %}

    -
    {% trans 'Unit Cost' %}
    +
    {% if min_total_bom_price %} @@ -76,14 +76,14 @@ {% 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 5ed17022f236639358402a99ea425f66f3565f53 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 6 May 2021 16:57:16 -0500 Subject: [PATCH 26/35] Fixed stock import as it was not validating the serial properly (#1559) --- InvenTree/stock/admin.py | 2 +- InvenTree/stock/models.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 5f3c08839d..3aa0ee451f 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -56,7 +56,7 @@ class LocationAdmin(ImportExportModelAdmin): class StockItemResource(ModelResource): """ Class for managing StockItem data import/export """ - # Custom manaegrs for ForeignKey fields + # Custom managers for ForeignKey fields part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) part_name = Field(attribute='part__full_name', readonly=True) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7f22b7a4ef..0d996c5a7c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -228,7 +228,7 @@ class StockItem(MPTTModel): super(StockItem, self).validate_unique(exclude) # If the serial number is set, make sure it is not a duplicate - if self.serial is not None: + if self.serial: # Query to look for duplicate serial numbers parts = PartModels.Part.objects.filter(tree_id=self.part.tree_id) stock = StockItem.objects.filter(part__in=parts, serial=self.serial) @@ -281,7 +281,7 @@ class StockItem(MPTTModel): if self.part is not None: # A part with a serial number MUST have the quantity set to 1 - if self.serial is not None: + if self.serial: if self.quantity > 1: raise ValidationError({ 'quantity': _('Quantity must be 1 for item with a serial number'), From 985967fccb05aeab28aa3ec1e25c582aade5fd57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 07:13:23 +0200 Subject: [PATCH 27/35] 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 d9c28457d2..40f57d247c 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1582,22 +1582,26 @@ class LineItemPricing(PartPricing): 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 @@ class LineItemPricing(PartPricing): 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 28/35] 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 40f57d247c..81c2082c1c 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 @@ class LineItemPricing(PartPricing): 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 @@ class LineItemPricing(PartPricing): 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 29/35] 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 004e81c000..8d34d790d8 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 9850f482c5..e394b28314 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 30/35] 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 09c15eb865..e4a399a0e1 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -411,6 +411,9 @@ function setupCallbacks() { 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 81c2082c1c..5a1be6e5be 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -6,6 +6,7 @@ Django views for interacting with Order app 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 @@ class LineItemPricing(PartPricing): 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 31/35] 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 040f32d441..710b434c82 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2067,10 +2067,14 @@ class PartPricing(AjaxView): # 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 32/35] 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 5a1be6e5be..4079080d66 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1633,23 +1633,22 @@ class LineItemPricing(PartPricing): 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 @@ class LineItemPricing(PartPricing): # 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 710b434c82..ad98095fb1 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2070,7 +2070,7 @@ class PartPricing(AjaxView): # 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 bdd61fdb2c3870d04fe58de73352c197f759a5db Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Fri, 7 May 2021 15:18:06 +0200 Subject: [PATCH 33/35] enable live-serving of i18n_static tag in debug (#1560) * enable live-serving of i18n_static tag in debug * style repair --- .../part/templatetags/inventree_extras.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 536f25cb5b..d8b98c53a7 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -200,18 +200,28 @@ class I18nStaticNode(StaticNode): return ret -@register.tag('i18n_static') -def do_i18n_static(parser, token): - """ - Overrides normal static, adds language - lookup for prerenderd files #1485 +# use the dynamic url - tag if in Debugging-Mode +if settings.DEBUG: - usage (like static): - {% i18n_static path [as varname] %} - """ - bits = token.split_contents() - loc_name = settings.STATICFILES_I18_PREFIX + @register.simple_tag() + def i18n_static(url_name): + """ simple tag to enable {% url %} functionality instead of {% static %} """ + return reverse(url_name) - # change path to called ressource - bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'" - token.contents = ' '.join(bits) - return I18nStaticNode.handle_token(parser, token) +else: + + @register.tag('i18n_static') + def do_i18n_static(parser, token): + """ + Overrides normal static, adds language - lookup for prerenderd files #1485 + + usage (like static): + {% i18n_static path [as varname] %} + """ + bits = token.split_contents() + loc_name = settings.STATICFILES_I18_PREFIX + + # change path to called ressource + bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'" + token.contents = ' '.join(bits) + return I18nStaticNode.handle_token(parser, token) From b6043af7c0f3bd46a261af2eda27b818be4e23ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 15:35:35 +0200 Subject: [PATCH 34/35] 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 4079080d66..29f70511b6 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1247,6 +1247,17 @@ class SOLineItemCreate(AjaxCreateView): 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 35/35] 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 29f70511b6..6913799631 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1259,6 +1259,7 @@ class SOLineItemCreate(AjaxCreateView): self.object = ret return ret + class SOLineItemEdit(AjaxUpdateView): """ View for editing a SalesOrderLineItem """