mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Merge branch 'master' into spaces!
This commit is contained in:
		| @@ -466,6 +466,24 @@ | |||||||
|     background: #eee; |     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 { | .btn-glyph { | ||||||
|     padding-left: 6px; |     padding-left: 6px; | ||||||
|     padding-right: 6px; |     padding-right: 6px; | ||||||
|   | |||||||
| @@ -74,7 +74,7 @@ def validate_build_order_reference(value): | |||||||
|         match = re.search(pattern, value) |         match = re.search(pattern, value) | ||||||
|  |  | ||||||
|         if match is None: |         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): | def validate_purchase_order_reference(value): | ||||||
| @@ -88,7 +88,7 @@ def validate_purchase_order_reference(value): | |||||||
|         match = re.search(pattern, value) |         match = re.search(pattern, value) | ||||||
|  |  | ||||||
|         if match is None: |         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): | def validate_sales_order_reference(value): | ||||||
| @@ -102,7 +102,7 @@ def validate_sales_order_reference(value): | |||||||
|         match = re.search(pattern, value) |         match = re.search(pattern, value) | ||||||
|  |  | ||||||
|         if match is None: |         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): | def validate_tree_name(value): | ||||||
|   | |||||||
| @@ -158,6 +158,8 @@ $('#view-calendar').click(function() { | |||||||
|  |  | ||||||
|     $("#build-order-calendar").show(); |     $("#build-order-calendar").show(); | ||||||
|     $("#view-list").show(); |     $("#view-list").show(); | ||||||
|  |  | ||||||
|  |     calendar.render(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| $("#view-list").click(function() { | $("#view-list").click(function() { | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ These models are 'generic' and do not fit a particular business logic object. | |||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals | ||||||
|  |  | ||||||
| import os | import os | ||||||
|  | import decimal | ||||||
|  | import math | ||||||
|  |  | ||||||
| from django.db import models, transaction | from django.db import models, transaction | ||||||
| from django.db.utils import IntegrityError, OperationalError | from django.db.utils import IntegrityError, OperationalError | ||||||
| @@ -730,6 +732,72 @@ class PriceBreak(models.Model): | |||||||
|         return converted.amount |         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): | class ColorTheme(models.Model): | ||||||
|     """ Color Theme Setting """ |     """ Color Theme Setting """ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,8 +6,6 @@ Company database model definitions | |||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals | ||||||
|  |  | ||||||
| import os | import os | ||||||
| import decimal |  | ||||||
| import math |  | ||||||
|  |  | ||||||
| from django.utils.translation import ugettext_lazy as _ | from django.utils.translation import ugettext_lazy as _ | ||||||
| from django.core.validators import MinValueValidator | from django.core.validators import MinValueValidator | ||||||
| @@ -26,7 +24,6 @@ from markdownx.models import MarkdownxField | |||||||
| from stdimage.models import StdImageField | from stdimage.models import StdImageField | ||||||
|  |  | ||||||
| from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail | from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail | ||||||
| from InvenTree.helpers import normalize |  | ||||||
| from InvenTree.fields import InvenTreeURLField | from InvenTree.fields import InvenTreeURLField | ||||||
| from InvenTree.status_codes import PurchaseOrderStatus | from InvenTree.status_codes import PurchaseOrderStatus | ||||||
|  |  | ||||||
| @@ -558,70 +555,8 @@ class SupplierPart(models.Model): | |||||||
|             price=price |             price=price | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def get_price(self, quantity, moq=True, multiples=True, currency=None): |     get_price = common.models.get_price | ||||||
|         """ 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 |  | ||||||
|  |  | ||||||
|     def open_orders(self): |     def open_orders(self): | ||||||
|         """ Return a database query for PO line items for this SupplierPart, |         """ Return a database query for PO line items for this SupplierPart, | ||||||
|   | |||||||
| @@ -202,7 +202,7 @@ class CompanyImageDownloadFromURL(AjaxUpdateView): | |||||||
|  |  | ||||||
|         # Check for valid response code |         # Check for valid response code | ||||||
|         if not response.status_code == 200: |         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 |             return | ||||||
|  |  | ||||||
|         response.raw.decode_content = True |         response.raw.decode_content = True | ||||||
|   | |||||||
| @@ -211,6 +211,7 @@ class EditSalesOrderLineItemForm(HelperForm): | |||||||
|             'part', |             'part', | ||||||
|             'quantity', |             'quantity', | ||||||
|             'reference', |             'reference', | ||||||
|  |             'sale_price', | ||||||
|             'notes' |             'notes' | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								InvenTree/order/migrations/0045_auto_20210504_1946.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								InvenTree/order/migrations/0045_auto_20210504_1946.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -367,7 +367,7 @@ class PurchaseOrder(Order): | |||||||
|             stock.save() |             stock.save() | ||||||
|  |  | ||||||
|             text = _("Received items") |             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 |             # Add a new transaction note to the newly created stock item | ||||||
|             stock.addTransactionNote(text, user, note) |             stock.addTransactionNote(text, user, note) | ||||||
| @@ -672,12 +672,22 @@ class SalesOrderLineItem(OrderLineItem): | |||||||
|     Attributes: |     Attributes: | ||||||
|         order: Link to the SalesOrder that this line item belongs to |         order: Link to the SalesOrder that this line item belongs to | ||||||
|         part: Link to a Part object (may be null) |         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')) |     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}) |     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: |     class Meta: | ||||||
|         unique_together = [ |         unique_together = [ | ||||||
|         ] |         ] | ||||||
|   | |||||||
| @@ -278,6 +278,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): | |||||||
|     quantity = serializers.FloatField() |     quantity = serializers.FloatField() | ||||||
|     allocated = serializers.FloatField(source='allocated_quantity', read_only=True) |     allocated = serializers.FloatField(source='allocated_quantity', read_only=True) | ||||||
|     fulfilled = serializers.FloatField(source='fulfilled_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: |     class Meta: | ||||||
|         model = SalesOrderLineItem |         model = SalesOrderLineItem | ||||||
| @@ -294,6 +295,9 @@ class SOLineItemSerializer(InvenTreeModelSerializer): | |||||||
|             'order_detail', |             'order_detail', | ||||||
|             'part', |             'part', | ||||||
|             'part_detail', |             'part_detail', | ||||||
|  |             'sale_price', | ||||||
|  |             'sale_price_currency', | ||||||
|  |             'sale_price_string', | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -146,6 +146,8 @@ $('#view-calendar').click(function() { | |||||||
|  |  | ||||||
|     $("#purchase-order-calendar").show(); |     $("#purchase-order-calendar").show(); | ||||||
|     $("#view-list").show(); |     $("#view-list").show(); | ||||||
|  |  | ||||||
|  |     calendar.render(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| $("#view-list").click(function() { | $("#view-list").click(function() { | ||||||
|   | |||||||
| @@ -223,6 +223,14 @@ $("#so-lines-table").inventreeTable({ | |||||||
|             field: 'quantity', |             field: 'quantity', | ||||||
|             title: '{% trans "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', |             field: 'allocated', | ||||||
|             {% if order.status == SalesOrderStatus.PENDING %} |             {% if order.status == SalesOrderStatus.PENDING %} | ||||||
| @@ -289,6 +297,7 @@ $("#so-lines-table").inventreeTable({ | |||||||
|                         html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}'); |                         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" %}'); |                 html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}'); | ||||||
| @@ -388,6 +397,26 @@ 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' %}", | ||||||
|  |             { | ||||||
|  |                 submit_text: '{% trans "Calculate price" %}', | ||||||
|  |                 data: { | ||||||
|  |                     line_item: pk, | ||||||
|  |                     quantity: row.quantity, | ||||||
|  |                 }, | ||||||
|  |                 buttons: [{name: 'update_price', | ||||||
|  |                     title: '{% trans "Update Unit Price" %}'},], | ||||||
|  |                 success: reloadTable, | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
| @@ -144,6 +144,8 @@ $('#view-calendar').click(function() { | |||||||
|  |  | ||||||
|     $("#sales-order-calendar").show(); |     $("#sales-order-calendar").show(); | ||||||
|     $("#view-list").show(); |     $("#view-list").show(); | ||||||
|  |  | ||||||
|  |     calendar.render(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| $("#view-list").click(function() { | $("#view-list").click(function() { | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ purchase_order_urls = [ | |||||||
|     url(r'^new/', views.PurchaseOrderCreate.as_view(), name='po-create'), |     url(r'^new/', views.PurchaseOrderCreate.as_view(), name='po-create'), | ||||||
|  |  | ||||||
|     url(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'), |     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 |     # Display detail view for a single purchase order | ||||||
|     url(r'^(?P<pk>\d+)/', include(purchase_order_detail_urls)), |     url(r'^(?P<pk>\d+)/', include(purchase_order_detail_urls)), | ||||||
|   | |||||||
| @@ -6,13 +6,14 @@ Django views for interacting with Order app | |||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals | ||||||
|  |  | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
|  | from django.http.response import JsonResponse | ||||||
| from django.shortcuts import get_object_or_404 | from django.shortcuts import get_object_or_404 | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.translation import ugettext_lazy as _ | from django.utils.translation import ugettext_lazy as _ | ||||||
| from django.views.generic import DetailView, ListView, UpdateView | from django.views.generic import DetailView, ListView, UpdateView | ||||||
| from django.views.generic.edit import FormMixin | from django.views.generic.edit import FormMixin | ||||||
| from django.forms import HiddenInput | from django.forms import HiddenInput, IntegerField | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
| from decimal import Decimal, InvalidOperation | from decimal import Decimal, InvalidOperation | ||||||
| @@ -29,6 +30,7 @@ from part.models import Part | |||||||
| from common.models import InvenTreeSetting | from common.models import InvenTreeSetting | ||||||
|  |  | ||||||
| from . import forms as order_forms | from . import forms as order_forms | ||||||
|  | from part.views import PartPricing | ||||||
|  |  | ||||||
| from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView | from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView | ||||||
| from InvenTree.helpers import DownloadFile, str2bool | from InvenTree.helpers import DownloadFile, str2bool | ||||||
| @@ -1245,6 +1247,18 @@ class SOLineItemCreate(AjaxCreateView): | |||||||
|  |  | ||||||
|         return initials |         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): | class SOLineItemEdit(AjaxUpdateView): | ||||||
|     """ View for editing a SalesOrderLineItem """ |     """ View for editing a SalesOrderLineItem """ | ||||||
| @@ -1407,7 +1421,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): | |||||||
|                 except StockItem.DoesNotExist: |                 except StockItem.DoesNotExist: | ||||||
|                     self.form.add_error( |                     self.form.add_error( | ||||||
|                         'serials', |                         'serials', | ||||||
|                         _('No matching item for serial') + f" '{serial}'" |                         _('No matching item for serial {serial}').format(serial=serial) | ||||||
|                     ) |                     ) | ||||||
|                     continue |                     continue | ||||||
|  |  | ||||||
| @@ -1417,7 +1431,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): | |||||||
|                 if not stock_item.in_stock: |                 if not stock_item.in_stock: | ||||||
|                     self.form.add_error( |                     self.form.add_error( | ||||||
|                         'serials', |                         'serials', | ||||||
|                         f"'{serial}' " + _("is not in stock") |                         _('{serial} is not in stock').format(serial=serial) | ||||||
|                     ) |                     ) | ||||||
|                     continue |                     continue | ||||||
|  |  | ||||||
| @@ -1425,7 +1439,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): | |||||||
|                 if stock_item.is_allocated(): |                 if stock_item.is_allocated(): | ||||||
|                     self.form.add_error( |                     self.form.add_error( | ||||||
|                         'serials', |                         'serials', | ||||||
|                         f"'{serial}' " + _("already allocated to an order") |                         _('{serial} already allocated to an order').format(serial=serial) | ||||||
|                     ) |                     ) | ||||||
|                     continue |                     continue | ||||||
|  |  | ||||||
| @@ -1571,3 +1585,101 @@ class SalesOrderAllocationDelete(AjaxDeleteView): | |||||||
|     ajax_form_title = _("Remove allocation") |     ajax_form_title = _("Remove allocation") | ||||||
|     context_object_name = 'allocation' |     context_object_name = 'allocation' | ||||||
|     ajax_template_name = "order/so_allocation_delete.html" |     ajax_template_name = "order/so_allocation_delete.html" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LineItemPricing(PartPricing): | ||||||
|  |     """ View for inspecting part pricing information """ | ||||||
|  |  | ||||||
|  |     class EnhancedForm(PartPricing.form_class): | ||||||
|  |         pk = IntegerField(widget=HiddenInput()) | ||||||
|  |         so_line = IntegerField(widget=HiddenInput()) | ||||||
|  |  | ||||||
|  |     form_class = EnhancedForm | ||||||
|  |  | ||||||
|  |     def get_part(self, id=False): | ||||||
|  |         if 'line_item' in self.request.GET: | ||||||
|  |             try: | ||||||
|  |                 part_id = self.request.GET.get('line_item') | ||||||
|  |                 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') | ||||||
|  |                 part = Part.objects.get(id=part_id) | ||||||
|  |             except Part.DoesNotExist: | ||||||
|  |                 return None | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         if id: | ||||||
|  |             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)) | ||||||
|  |         if qty == 1: | ||||||
|  |             return Decimal(self.request.POST.get('quantity', 1)) | ||||||
|  |         return qty | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |  | ||||||
|  |     def post(self, request, *args, **kwargs): | ||||||
|  |         # 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) | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								InvenTree/part/migrations/0065_auto_20210505_2144.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								InvenTree/part/migrations/0065_auto_20210505_2144.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -1611,6 +1611,44 @@ class Part(MPTTModel): | |||||||
|                 max(buy_price_range[1], bom_price_range[1]) |                 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 |     @transaction.atomic | ||||||
|     def copy_bom_from(self, other, clear=True, **kwargs): |     def copy_bom_from(self, other, clear=True, **kwargs): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -91,7 +91,7 @@ | |||||||
|     {% if part.salable and roles.sales_order.view %} |     {% if part.salable and roles.sales_order.view %} | ||||||
|     <li class='list-group-item {% if tab == "sales-prices" %}active{% endif %}' title='{% trans "Sales Price Information" %}'> |     <li class='list-group-item {% if tab == "sales-prices" %}active{% endif %}' title='{% trans "Sales Price Information" %}'> | ||||||
|         <a href='{% url "part-sale-prices" part.id %}'> |         <a href='{% url "part-sale-prices" part.id %}'> | ||||||
|             <span class='menu-tab-icon fas fa-dollar-sign'></span> |             <span class='menu-tab-icon fas fa-dollar-sign' style='width: 20px;'></span> | ||||||
|             {% trans "Sale Price" %} |             {% trans "Sale Price" %} | ||||||
|         </a> |         </a> | ||||||
|     </li> |     </li> | ||||||
|   | |||||||
| @@ -4,24 +4,20 @@ | |||||||
|  |  | ||||||
| {% block pre_form_content %} | {% block pre_form_content %} | ||||||
|  |  | ||||||
| <div class='alert alert-info alert-block'> | <table class='table table-striped table-condensed table-price-two'> | ||||||
| {% blocktrans %}Pricing information for:<br>{{part}}.{% endblocktrans %} |  | ||||||
| </div> |  | ||||||
|  |  | ||||||
| <h4>{% trans 'Quantity' %}</h4> |  | ||||||
| <table class='table table-striped table-condensed'> |  | ||||||
|     <tr> |     <tr> | ||||||
|         <td><b>{% trans 'Part' %}</b></td> |         <td><b>{% trans 'Part' %}</b></td> | ||||||
|         <td colspan='2'>{{ part  }}</td> |         <td>{{ part  }}</td> | ||||||
|     </tr> |     </tr> | ||||||
|     <tr> |     <tr> | ||||||
|         <td><b>{% trans 'Quantity' %}</b></td> |         <td><b>{% trans 'Quantity' %}</b></td> | ||||||
|         <td colspan='2'>{{ quantity }}</td> |         <td>{{ quantity }}</td> | ||||||
|     </tr> |     </tr> | ||||||
| </table> | </table> | ||||||
|     {% if part.supplier_count > 0 %} |  | ||||||
|  | {% if part.supplier_count > 0 %} | ||||||
|     <h4>{% trans 'Supplier Pricing' %}</h4> |     <h4>{% trans 'Supplier Pricing' %}</h4> | ||||||
| <table class='table table-striped table-condensed'> |     <table class='table table-striped table-condensed table-price-three'> | ||||||
|     {% if min_total_buy_price %} |     {% if min_total_buy_price %} | ||||||
|     <tr> |     <tr> | ||||||
|         <td><b>{% trans 'Unit Cost' %}</b></td> |         <td><b>{% trans 'Unit Cost' %}</b></td> | ||||||
| @@ -42,12 +38,12 @@ | |||||||
|         </td> |         </td> | ||||||
|     </tr> |     </tr> | ||||||
|     {% endif %} |     {% endif %} | ||||||
| </table> |     </table> | ||||||
|     {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|     {% if part.bom_count > 0 %} | {% if part.bom_count > 0 %} | ||||||
|     <h4>{% trans 'BOM Pricing' %}</h4> |     <h4>{% trans 'BOM Pricing' %}</h4> | ||||||
| <table class='table table-striped table-condensed'> |     <table class='table table-striped table-condensed table-price-three'> | ||||||
|     {% if min_total_bom_price %} |     {% if min_total_bom_price %} | ||||||
|     <tr> |     <tr> | ||||||
|         <td><b>{% trans 'Unit Cost' %}</b></td> |         <td><b>{% trans 'Unit Cost' %}</b></td> | ||||||
| @@ -75,8 +71,22 @@ | |||||||
|         </td> |         </td> | ||||||
|     </tr> |     </tr> | ||||||
|     {% endif %} |     {% endif %} | ||||||
| </table> |     </table> | ||||||
|     {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|  | {% if total_part_price %} | ||||||
|  |     <h4>{% trans 'Sale Price' %}</h4> | ||||||
|  |     <table class='table table-striped table-condensed table-price-two'> | ||||||
|  |         <tr> | ||||||
|  |             <td><b>{% trans 'Unit Cost' %}</b></td> | ||||||
|  |             <td>{% include "price.html" with price=unit_part_price %}</td> | ||||||
|  |         </tr> | ||||||
|  |         <tr> | ||||||
|  |             <td><b>{% trans 'Total Cost' %}</b></td> | ||||||
|  |             <td>{% include "price.html" with price=total_part_price %}</td> | ||||||
|  |         </tr> | ||||||
|  |     </table> | ||||||
|  | {% endif %} | ||||||
|  |  | ||||||
| {% if min_unit_buy_price or min_unit_bom_price %} | {% if min_unit_buy_price or min_unit_bom_price %} | ||||||
| {% else %} | {% else %} | ||||||
| @@ -84,7 +94,5 @@ | |||||||
|     {% trans 'No pricing information is available for this part.' %} |     {% trans 'No pricing information is available for this part.' %} | ||||||
| </div> | </div> | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
| <hr> | <hr> | ||||||
|  |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
| @@ -2,7 +2,7 @@ | |||||||
| {% load static %} | {% load static %} | ||||||
| {% load i18n %} | {% load i18n %} | ||||||
|  |  | ||||||
| {% block menubar %}} | {% block menubar %} | ||||||
| {% include 'part/navbar.html' with tab='sales-prices' %} | {% include 'part/navbar.html' with tab='sales-prices' %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -200,8 +200,18 @@ class I18nStaticNode(StaticNode): | |||||||
|         return ret |         return ret | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.tag('i18n_static') | # use the dynamic url - tag if in Debugging-Mode | ||||||
| def do_i18n_static(parser, token): | if settings.DEBUG: | ||||||
|  |  | ||||||
|  |     @register.simple_tag() | ||||||
|  |     def i18n_static(url_name): | ||||||
|  |         """ simple tag to enable {% url %} functionality instead of {% static %} """ | ||||||
|  |         return reverse(url_name) | ||||||
|  |  | ||||||
|  | else: | ||||||
|  |  | ||||||
|  |     @register.tag('i18n_static') | ||||||
|  |     def do_i18n_static(parser, token): | ||||||
|         """ |         """ | ||||||
|         Overrides normal static, adds language - lookup for prerenderd files #1485 |         Overrides normal static, adds language - lookup for prerenderd files #1485 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,7 +30,6 @@ sale_price_break_urls = [ | |||||||
| ] | ] | ||||||
|  |  | ||||||
| part_parameter_urls = [ | part_parameter_urls = [ | ||||||
|  |  | ||||||
|     url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), |     url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), | ||||||
|     url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'), |     url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'), | ||||||
|     url(r'^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), |     url(r'^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), | ||||||
|   | |||||||
| @@ -884,7 +884,7 @@ class PartImageDownloadFromURL(AjaxUpdateView): | |||||||
|  |  | ||||||
|         # Check for valid response code |         # Check for valid response code | ||||||
|         if not response.status_code == 200: |         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 |             return | ||||||
|  |  | ||||||
|         response.raw.decode_content = True |         response.raw.decode_content = True | ||||||
| @@ -1959,7 +1959,6 @@ class PartPricing(AjaxView): | |||||||
|  |  | ||||||
|     def get_quantity(self): |     def get_quantity(self): | ||||||
|         """ Return set quantity in decimal format """ |         """ Return set quantity in decimal format """ | ||||||
|  |  | ||||||
|         return Decimal(self.request.POST.get('quantity', 1)) |         return Decimal(self.request.POST.get('quantity', 1)) | ||||||
|  |  | ||||||
|     def get_part(self): |     def get_part(self): | ||||||
| @@ -1969,12 +1968,7 @@ class PartPricing(AjaxView): | |||||||
|             return None |             return None | ||||||
|  |  | ||||||
|     def get_pricing(self, quantity=1, currency=None): |     def get_pricing(self, quantity=1, currency=None): | ||||||
|  |         """ returns context with pricing information """ | ||||||
|         # try: |  | ||||||
|         #     quantity = int(quantity) |  | ||||||
|         # except ValueError: |  | ||||||
|         #     quantity = 1 |  | ||||||
|  |  | ||||||
|         if quantity <= 0: |         if quantity <= 0: | ||||||
|             quantity = 1 |             quantity = 1 | ||||||
|  |  | ||||||
| @@ -2044,11 +2038,22 @@ class PartPricing(AjaxView): | |||||||
|                     ctx['max_total_bom_price'] = max_bom_price |                     ctx['max_total_bom_price'] = max_bom_price | ||||||
|                     ctx['max_unit_bom_price'] = max_unit_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 |         return ctx | ||||||
|  |  | ||||||
|     def get(self, request, *args, **kwargs): |     def get_initials(self): | ||||||
|  |         """ returns initials for form """ | ||||||
|  |         return {'quantity': self.get_quantity()} | ||||||
|  |  | ||||||
|         return self.renderJsonResponse(request, self.form_class(), context=self.get_pricing()) |     def get(self, request, *args, **kwargs): | ||||||
|  |         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): |     def post(self, request, *args, **kwargs): | ||||||
|  |  | ||||||
| @@ -2057,16 +2062,19 @@ class PartPricing(AjaxView): | |||||||
|         quantity = self.get_quantity() |         quantity = self.get_quantity() | ||||||
|  |  | ||||||
|         # Retain quantity value set by user |         # Retain quantity value set by user | ||||||
|         form = self.form_class() |         form = self.form_class(initial=self.get_initials()) | ||||||
|         form.fields['quantity'].initial = quantity |  | ||||||
|  |  | ||||||
|         # TODO - How to handle pricing in different currencies? |         # TODO - How to handle pricing in different currencies? | ||||||
|         currency = None |         currency = None | ||||||
|  |  | ||||||
|  |         # check if data is set | ||||||
|  |         try: | ||||||
|  |             data = self.data | ||||||
|  |         except AttributeError: | ||||||
|  |             data = {} | ||||||
|  |  | ||||||
|         # Always mark the form as 'invalid' (the user may wish to keep getting pricing data) |         # Always mark the form as 'invalid' (the user may wish to keep getting pricing data) | ||||||
|         data = { |         data['form_valid'] = False | ||||||
|             'form_valid': False, |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return self.renderJsonResponse(request, form, data=data, context=self.get_pricing(quantity, currency)) |         return self.renderJsonResponse(request, form, data=data, context=self.get_pricing(quantity, currency)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ class LocationAdmin(ImportExportModelAdmin): | |||||||
| class StockItemResource(ModelResource): | class StockItemResource(ModelResource): | ||||||
|     """ Class for managing StockItem data import/export """ |     """ 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 = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) | ||||||
|  |  | ||||||
|     part_name = Field(attribute='part__full_name', readonly=True) |     part_name = Field(attribute='part__full_name', readonly=True) | ||||||
|   | |||||||
| @@ -198,7 +198,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|         if add_note: |         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 |             # This StockItem is being saved for the first time | ||||||
|             self.addTransactionNote( |             self.addTransactionNote( | ||||||
| @@ -228,7 +228,7 @@ class StockItem(MPTTModel): | |||||||
|         super(StockItem, self).validate_unique(exclude) |         super(StockItem, self).validate_unique(exclude) | ||||||
|  |  | ||||||
|         # If the serial number is set, make sure it is not a duplicate |         # 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 |             # Query to look for duplicate serial numbers | ||||||
|             parts = PartModels.Part.objects.filter(tree_id=self.part.tree_id) |             parts = PartModels.Part.objects.filter(tree_id=self.part.tree_id) | ||||||
|             stock = StockItem.objects.filter(part__in=parts, serial=self.serial) |             stock = StockItem.objects.filter(part__in=parts, serial=self.serial) | ||||||
| @@ -281,7 +281,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|             if self.part is not None: |             if self.part is not None: | ||||||
|                 # A part with a serial number MUST have the quantity set to 1 |                 # 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: |                     if self.quantity > 1: | ||||||
|                         raise ValidationError({ |                         raise ValidationError({ | ||||||
|                             'quantity': _('Quantity must be 1 for item with a serial number'), |                             'quantity': _('Quantity must be 1 for item with a serial number'), | ||||||
| @@ -613,7 +613,7 @@ class StockItem(MPTTModel): | |||||||
|         item.addTransactionNote( |         item.addTransactionNote( | ||||||
|             _("Assigned to Customer"), |             _("Assigned to Customer"), | ||||||
|             user, |             user, | ||||||
|             notes=_("Manually assigned to customer") + " " + customer.name, |             notes=_("Manually assigned to customer {name}").format(name=customer.name), | ||||||
|             system=True |             system=True | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -626,9 +626,9 @@ class StockItem(MPTTModel): | |||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         self.addTransactionNote( |         self.addTransactionNote( | ||||||
|             _("Returned from customer") + f" {self.customer.name}", |             _("Returned from customer {name}").format(name=self.customer.name), | ||||||
|             user, |             user, | ||||||
|             notes=_("Returned to location") + f" {location.name}", |             notes=_("Returned to location {loc}").format(loc=location.name), | ||||||
|             system=True |             system=True | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -789,7 +789,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|         # Add a transaction note to the other item |         # Add a transaction note to the other item | ||||||
|         stock_item.addTransactionNote( |         stock_item.addTransactionNote( | ||||||
|             _('Installed into stock item') + ' ' + str(self.pk), |             _('Installed into stock item {pk}').format(str(self.pk)), | ||||||
|             user, |             user, | ||||||
|             notes=notes, |             notes=notes, | ||||||
|             url=self.get_absolute_url() |             url=self.get_absolute_url() | ||||||
| @@ -797,7 +797,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|         # Add a transaction note to this item |         # Add a transaction note to this item | ||||||
|         self.addTransactionNote( |         self.addTransactionNote( | ||||||
|             _('Installed stock item') + ' ' + str(stock_item.pk), |             _('Installed stock item {pk}').format(str(stock_item.pk)), | ||||||
|             user, notes=notes, |             user, notes=notes, | ||||||
|             url=stock_item.get_absolute_url() |             url=stock_item.get_absolute_url() | ||||||
|         ) |         ) | ||||||
| @@ -821,7 +821,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|         # Add a transaction note to the parent item |         # Add a transaction note to the parent item | ||||||
|         self.belongs_to.addTransactionNote( |         self.belongs_to.addTransactionNote( | ||||||
|             _("Uninstalled stock item") + ' ' + str(self.pk), |             _("Uninstalled stock item {pk}").format(pk=str(self.pk)), | ||||||
|             user, |             user, | ||||||
|             notes=notes, |             notes=notes, | ||||||
|             url=self.get_absolute_url(), |             url=self.get_absolute_url(), | ||||||
| @@ -840,7 +840,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|         # Add a transaction note! |         # Add a transaction note! | ||||||
|         self.addTransactionNote( |         self.addTransactionNote( | ||||||
|             _('Uninstalled into location') + ' ' + str(location), |             _('Uninstalled into location {loc}').formaT(loc=str(location)), | ||||||
|             user, |             user, | ||||||
|             notes=notes, |             notes=notes, | ||||||
|             url=url |             url=url | ||||||
| @@ -966,7 +966,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|         if len(existing) > 0: |         if len(existing) > 0: | ||||||
|             exists = ','.join([str(x) for x in existing]) |             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 |         # Create a new stock item for each unique serial number | ||||||
|         for serial in serials: |         for serial in serials: | ||||||
| @@ -1074,7 +1074,7 @@ class StockItem(MPTTModel): | |||||||
|         new_stock.addTransactionNote( |         new_stock.addTransactionNote( | ||||||
|             _("Split from existing stock"), |             _("Split from existing stock"), | ||||||
|             user, |             user, | ||||||
|             f"{_('Split')} {helpers.normalize(quantity)} {_('items')}" |             _('Split {n} items').format(n=helpers.normalize(quantity)) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Remove the specified quantity from THIS stock item |         # Remove the specified quantity from THIS stock item | ||||||
| @@ -1131,10 +1131,10 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|             return True |             return True | ||||||
|  |  | ||||||
|         msg = f"{_('Moved to')} {str(location)}" |  | ||||||
|  |  | ||||||
|         if self.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 |         self.location = location | ||||||
|  |  | ||||||
| @@ -1202,9 +1202,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|         if self.updateQuantity(count): |         if self.updateQuantity(count): | ||||||
|  |  | ||||||
|             n = helpers.normalize(count) |             text = _('Counted {n} items').format(n=helpers.normalize(count)) | ||||||
|  |  | ||||||
|             text = f"{_('Counted')} {n} {_('items')}" |  | ||||||
|  |  | ||||||
|             self.addTransactionNote( |             self.addTransactionNote( | ||||||
|                 text, |                 text, | ||||||
| @@ -1236,9 +1234,7 @@ class StockItem(MPTTModel): | |||||||
|             return False |             return False | ||||||
|  |  | ||||||
|         if self.updateQuantity(self.quantity + quantity): |         if self.updateQuantity(self.quantity + quantity): | ||||||
|  |             text = _('Added {n} items').format(n=helpers.normalize(quantity)) | ||||||
|             n = helpers.normalize(quantity) |  | ||||||
|             text = f"{_('Added')} {n} {_('items')}" |  | ||||||
|  |  | ||||||
|             self.addTransactionNote( |             self.addTransactionNote( | ||||||
|                 text, |                 text, | ||||||
| @@ -1268,8 +1264,7 @@ class StockItem(MPTTModel): | |||||||
|  |  | ||||||
|         if self.updateQuantity(self.quantity - quantity): |         if self.updateQuantity(self.quantity - quantity): | ||||||
|  |  | ||||||
|             q = helpers.normalize(quantity) |             text = _('Removed {n1} items').format(n1=helpers.normalize(quantity)) | ||||||
|             text = f"{_('Removed')} {q} {_('items')}" |  | ||||||
|  |  | ||||||
|             self.addTransactionNote(text, |             self.addTransactionNote(text, | ||||||
|                                     user, |                                     user, | ||||||
|   | |||||||
| @@ -377,6 +377,15 @@ function modalSubmit(modal, callback) { | |||||||
|     $(modal).on('click', '#modal-form-submit', function() { |     $(modal).on('click', '#modal-form-submit', function() { | ||||||
|         callback(); |         callback(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     $(modal).on('click', '.modal-form-button', function() { | ||||||
|  |         // Append data to form | ||||||
|  |         var name = $(this).attr('name'); | ||||||
|  |         var value = $(this).attr('value'); | ||||||
|  |         var input = '<input id="id_act-btn_' + name + '" type="hidden" name="act-btn_' + name + '" value="' + value + '">'; | ||||||
|  |         $('.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 = "<span style='float: right;'>"; | ||||||
|  |     html += "<button name='" + options.name + "' type='submit' class='btn btn-default modal-form-button'"; | ||||||
|  |     html += " value='" + options.name + "'>" + options.title + "</button>"; | ||||||
|  |     html += "</span>"; | ||||||
|  |  | ||||||
|  |     $(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) { | function attachFieldCallback(modal, callback) { | ||||||
|     /* Attach a 'callback' function to a given field in the modal form. |     /* 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 submit_text = options.submit_text || '{% trans "Submit" %}'; | ||||||
|     var close_text = options.close_text || '{% trans "Close" %}'; |     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 |     // Form the ajax request to retrieve the django form data | ||||||
|     ajax_data = { |     ajax_data = { | ||||||
|         url: url, |         url: url, | ||||||
| @@ -852,6 +883,10 @@ function launchModalForm(url, options = {}) { | |||||||
|                     handleModalForm(url, options); |                     handleModalForm(url, options); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 if (options.buttons) { | ||||||
|  |                     attachButtons(modal, options.buttons); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|             } else { |             } else { | ||||||
|                 $(modal).modal('hide'); |                 $(modal).modal('hide'); | ||||||
|                 showAlertDialog('{% trans "Invalid server response" %}', '{% trans "JSON response missing form data" %}'); |                 showAlertDialog('{% trans "Invalid server response" %}', '{% trans "JSON response missing form data" %}'); | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class='modal-footer'> |             <div class='modal-footer'> | ||||||
|  |                 <div id='modal-footer-buttons'></div> | ||||||
|                 <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> |                 <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> | ||||||
|                 <button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button> |                 <button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button> | ||||||
|             </div> |             </div> | ||||||
| @@ -49,6 +50,7 @@ | |||||||
|                   </div> |                   </div> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class='modal-footer'> |                 <div class='modal-footer'> | ||||||
|  |                     <div id='modal-footer-buttons'></div> | ||||||
|                     <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> |                     <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> | ||||||
|                     <button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button> |                     <button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button> | ||||||
|                 </div> |                 </div> | ||||||
| @@ -69,6 +71,7 @@ | |||||||
|                 <div class='modal-form-content'> |                 <div class='modal-form-content'> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class='modal-footer'> |                 <div class='modal-footer'> | ||||||
|  |                     <div id='modal-footer-buttons'></div> | ||||||
|                     <button type='button' class='btn btn-default' id='modal-form-cancel' data-dismiss='modal'>{% trans "Cancel" %}</button> |                     <button type='button' class='btn btn-default' id='modal-form-cancel' data-dismiss='modal'>{% trans "Cancel" %}</button> | ||||||
|                     <button type='button' class='btn btn-primary' id='modal-form-accept'>{% trans "Accept" %}</button> |                     <button type='button' class='btn btn-primary' id='modal-form-accept'>{% trans "Accept" %}</button> | ||||||
|                 </div> |                 </div> | ||||||
| @@ -90,6 +93,7 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class='modal-footer'> |             <div class='modal-footer'> | ||||||
|  |                 <div id='modal-footer-buttons'></div> | ||||||
|                 <button type='button' class='btn btn-default' data-dismiss='modal'>{% trans "Close" %}</button> |                 <button type='button' class='btn btn-default' data-dismiss='modal'>{% trans "Close" %}</button> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user