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; | ||||
| } | ||||
|  | ||||
| /* 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; | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -158,6 +158,8 @@ $('#view-calendar').click(function() { | ||||
|  | ||||
|     $("#build-order-calendar").show(); | ||||
|     $("#view-list").show(); | ||||
|  | ||||
|     calendar.render(); | ||||
| }); | ||||
|  | ||||
| $("#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 | ||||
|  | ||||
| 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 """ | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| @@ -558,70 +555,8 @@ 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. | ||||
|     get_price = common.models.get_price | ||||
|  | ||||
|         - 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): | ||||
|         """ Return a database query for PO line items for this SupplierPart, | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -211,6 +211,7 @@ class EditSalesOrderLineItemForm(HelperForm): | ||||
|             'part', | ||||
|             'quantity', | ||||
|             'reference', | ||||
|             'sale_price', | ||||
|             '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() | ||||
|  | ||||
|             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) | ||||
| @@ -672,12 +672,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 = [ | ||||
|         ] | ||||
|   | ||||
| @@ -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', | ||||
|         ] | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -146,6 +146,8 @@ $('#view-calendar').click(function() { | ||||
|  | ||||
|     $("#purchase-order-calendar").show(); | ||||
|     $("#view-list").show(); | ||||
|  | ||||
|     calendar.render(); | ||||
| }); | ||||
|  | ||||
| $("#view-list").click(function() { | ||||
|   | ||||
| @@ -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 %} | ||||
| @@ -289,6 +297,7 @@ $("#so-lines-table").inventreeTable({ | ||||
|                         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" %}'); | ||||
| @@ -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 %} | ||||
| @@ -144,6 +144,8 @@ $('#view-calendar').click(function() { | ||||
|  | ||||
|     $("#sales-order-calendar").show(); | ||||
|     $("#view-list").show(); | ||||
|  | ||||
|     calendar.render(); | ||||
| }); | ||||
|  | ||||
| $("#view-list").click(function() { | ||||
|   | ||||
| @@ -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<pk>\d+)/', include(purchase_order_detail_urls)), | ||||
|   | ||||
| @@ -6,13 +6,14 @@ 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 | ||||
| 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 | ||||
| @@ -29,6 +30,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 | ||||
| @@ -1245,6 +1247,18 @@ 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 """ | ||||
| @@ -1407,7 +1421,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 +1431,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 +1439,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 | ||||
|  | ||||
| @@ -1571,3 +1585,101 @@ 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 """ | ||||
|  | ||||
|     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]) | ||||
|             ) | ||||
|  | ||||
|     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): | ||||
|         """ | ||||
|   | ||||
| @@ -91,7 +91,7 @@ | ||||
|     {% if part.salable and roles.sales_order.view %} | ||||
|     <li class='list-group-item {% if tab == "sales-prices" %}active{% endif %}' title='{% trans "Sales Price Information" %}'> | ||||
|         <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" %} | ||||
|         </a> | ||||
|     </li> | ||||
|   | ||||
| @@ -4,24 +4,20 @@ | ||||
|  | ||||
| {% block pre_form_content %} | ||||
|  | ||||
| <div class='alert alert-info alert-block'> | ||||
| {% blocktrans %}Pricing information for:<br>{{part}}.{% endblocktrans %} | ||||
| </div> | ||||
|  | ||||
| <h4>{% trans 'Quantity' %}</h4> | ||||
| <table class='table table-striped table-condensed'> | ||||
| <table class='table table-striped table-condensed table-price-two'> | ||||
|     <tr> | ||||
|         <td><b>{% trans 'Part' %}</b></td> | ||||
|         <td colspan='2'>{{ part  }}</td> | ||||
|         <td>{{ part  }}</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|         <td><b>{% trans 'Quantity' %}</b></td> | ||||
|         <td colspan='2'>{{ quantity }}</td> | ||||
|         <td>{{ quantity }}</td> | ||||
|     </tr> | ||||
| </table> | ||||
|     {% if part.supplier_count > 0 %} | ||||
|  | ||||
| {% if part.supplier_count > 0 %} | ||||
|     <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 %} | ||||
|     <tr> | ||||
|         <td><b>{% trans 'Unit Cost' %}</b></td> | ||||
| @@ -42,12 +38,12 @@ | ||||
|         </td> | ||||
|     </tr> | ||||
|     {% endif %} | ||||
| </table> | ||||
|     {% endif %} | ||||
|     </table> | ||||
| {% endif %} | ||||
|  | ||||
|     {% if part.bom_count > 0 %} | ||||
| {% if part.bom_count > 0 %} | ||||
|     <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 %} | ||||
|     <tr> | ||||
|         <td><b>{% trans 'Unit Cost' %}</b></td> | ||||
| @@ -75,8 +71,22 @@ | ||||
|         </td> | ||||
|     </tr> | ||||
|     {% endif %} | ||||
| </table> | ||||
|     {% endif %} | ||||
|     </table> | ||||
| {% 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 %} | ||||
| {% else %} | ||||
| @@ -84,7 +94,5 @@ | ||||
|     {% trans 'No pricing information is available for this part.' %} | ||||
| </div> | ||||
| {% endif %} | ||||
|  | ||||
| <hr> | ||||
|  | ||||
| {% endblock %} | ||||
| @@ -2,7 +2,7 @@ | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block menubar %}} | ||||
| {% block menubar %} | ||||
| {% include 'part/navbar.html' with tab='sales-prices' %} | ||||
| {% endblock %} | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -30,7 +30,6 @@ sale_price_break_urls = [ | ||||
| ] | ||||
|  | ||||
| part_parameter_urls = [ | ||||
|  | ||||
|     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+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), | ||||
|   | ||||
| @@ -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 | ||||
| @@ -1959,7 +1959,6 @@ class PartPricing(AjaxView): | ||||
|  | ||||
|     def get_quantity(self): | ||||
|         """ Return set quantity in decimal format """ | ||||
|  | ||||
|         return Decimal(self.request.POST.get('quantity', 1)) | ||||
|  | ||||
|     def get_part(self): | ||||
| @@ -1969,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 | ||||
|  | ||||
| @@ -2044,11 +2038,22 @@ 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): | ||||
|     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): | ||||
|  | ||||
| @@ -2057,16 +2062,19 @@ 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 | ||||
|  | ||||
|         # 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) | ||||
|         data = { | ||||
|             'form_valid': False, | ||||
|         } | ||||
|         data['form_valid'] = False | ||||
|  | ||||
|         return self.renderJsonResponse(request, form, data=data, context=self.get_pricing(quantity, currency)) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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( | ||||
| @@ -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'), | ||||
| @@ -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,7 @@ 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 +1264,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, | ||||
|   | ||||
| @@ -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 = '<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) { | ||||
|     /* 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" %}'); | ||||
|   | ||||
| @@ -25,6 +25,7 @@ | ||||
|               </div> | ||||
|             </div> | ||||
|             <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-primary' id='modal-form-submit'>{% trans "Submit" %}</button> | ||||
|             </div> | ||||
| @@ -49,6 +50,7 @@ | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <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-primary' id='modal-form-submit'>{% trans "Submit" %}</button> | ||||
|                 </div> | ||||
| @@ -69,6 +71,7 @@ | ||||
|                 <div class='modal-form-content'> | ||||
|                 </div> | ||||
|                 <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-primary' id='modal-form-accept'>{% trans "Accept" %}</button> | ||||
|                 </div> | ||||
| @@ -90,6 +93,7 @@ | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class='modal-footer'> | ||||
|                 <div id='modal-footer-buttons'></div> | ||||
|                 <button type='button' class='btn btn-default' data-dismiss='modal'>{% trans "Close" %}</button> | ||||
|             </div> | ||||
|         </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user