mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into price-history
This commit is contained in:
commit
4156b71c4b
@ -77,12 +77,20 @@ class AuthRequiredMiddleware(object):
|
|||||||
if request.path_info == reverse_lazy('logout'):
|
if request.path_info == reverse_lazy('logout'):
|
||||||
return HttpResponseRedirect(reverse_lazy('login'))
|
return HttpResponseRedirect(reverse_lazy('login'))
|
||||||
|
|
||||||
login = reverse_lazy('login')
|
path = request.path_info
|
||||||
|
|
||||||
if not request.path_info == login and not request.path_info.startswith('/api/'):
|
# List of URL endpoints we *do not* want to redirect to
|
||||||
|
urls = [
|
||||||
|
reverse_lazy('login'),
|
||||||
|
reverse_lazy('logout'),
|
||||||
|
reverse_lazy('admin:login'),
|
||||||
|
reverse_lazy('admin:logout'),
|
||||||
|
]
|
||||||
|
|
||||||
|
if path not in urls and not path.startswith('/api/'):
|
||||||
# Save the 'next' parameter to pass through to the login view
|
# Save the 'next' parameter to pass through to the login view
|
||||||
|
|
||||||
return redirect('%s?next=%s' % (login, request.path))
|
return redirect('%s?next=%s' % (reverse_lazy('login'), request.path))
|
||||||
|
|
||||||
# Code to be executed for each request/response after
|
# Code to be executed for each request/response after
|
||||||
# the view is called.
|
# the view is called.
|
||||||
|
@ -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;
|
||||||
@ -489,7 +507,7 @@
|
|||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
};
|
}
|
||||||
|
|
||||||
.panel-heading .badge {
|
.panel-heading .badge {
|
||||||
float: right;
|
float: right;
|
||||||
@ -550,7 +568,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.media {
|
.media {
|
||||||
//padding-top: 15px;
|
/* padding-top: 15px; */
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,8 +594,8 @@
|
|||||||
width: 160px;
|
width: 160px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
//top: 0;
|
/* top: 0;
|
||||||
//left: 0;
|
left: 0; */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-right: 25px;
|
padding-right: 25px;
|
||||||
@ -808,7 +826,7 @@ input[type="submit"] {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
z-index: 5000;
|
z-index: 5000;
|
||||||
pointer-events: none; // Prevent this div from blocking links underneath
|
pointer-events: none; /* Prevent this div from blocking links underneath */
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
@ -919,3 +937,14 @@ input[type="submit"] {
|
|||||||
input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control {
|
input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control {
|
||||||
line-height: unset;
|
line-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clip-btn {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 0px 6px;
|
||||||
|
color: var(--label-grey);
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clip-btn:hover {
|
||||||
|
background: var(--label-grey);
|
||||||
|
}
|
||||||
|
7
InvenTree/InvenTree/static/script/clipboard.min.js
vendored
Normal file
7
InvenTree/InvenTree/static/script/clipboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,3 +1,14 @@
|
|||||||
|
function attachClipboard(selector) {
|
||||||
|
|
||||||
|
new ClipboardJS(selector, {
|
||||||
|
text: function(trigger) {
|
||||||
|
var content = trigger.parentElement.parentElement.textContent;
|
||||||
|
|
||||||
|
return content.trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function inventreeDocReady() {
|
function inventreeDocReady() {
|
||||||
/* Run this function when the HTML document is loaded.
|
/* Run this function when the HTML document is loaded.
|
||||||
* This will be called for every page that extends "base.html"
|
* This will be called for every page that extends "base.html"
|
||||||
@ -48,6 +59,10 @@ function inventreeDocReady() {
|
|||||||
no_post: true,
|
no_post: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize clipboard-buttons
|
||||||
|
attachClipboard('.clip-btn');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFileTransfer(transfer) {
|
function isFileTransfer(transfer) {
|
||||||
|
@ -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
|
||||||
@ -737,6 +739,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,7 @@ 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
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends "modal_delete_form.html" %}
|
{% extends "modal_delete_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
Are you sure you wish to delete this line item?
|
{% trans "Are you sure you wish to delete this line item?" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -20,20 +20,20 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-font'></span></td>
|
<td><span class='fas fa-font'></span></td>
|
||||||
<td><strong>{% trans "Part name" %}</strong></td>
|
<td><strong>{% trans "Part name" %}</strong></td>
|
||||||
<td>{{ part.name }}</td>
|
<td>{{ part.name }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if part.IPN %}
|
{% if part.IPN %}
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><strong>{% trans "IPN" %}</strong></td>
|
<td><strong>{% trans "IPN" %}</strong></td>
|
||||||
<td>{{ part.IPN }}</td>
|
<td>{{ part.IPN }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.revision %}
|
{% if part.revision %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-code-branch'></span></td>
|
<td><span class='fas fa-code-branch'></span></td>
|
||||||
<td><strong>{% trans "Revision" %}</strong></td>
|
<td><strong>{% trans "Revision" %}</strong></td>
|
||||||
<td>{{ part.revision }}</td>
|
<td>{{ part.revision }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.trackable %}
|
{% if part.trackable %}
|
||||||
@ -42,7 +42,7 @@
|
|||||||
<td><strong>{% trans "Latest Serial Number" %}</strong></td>
|
<td><strong>{% trans "Latest Serial Number" %}</strong></td>
|
||||||
<td>
|
<td>
|
||||||
{% if part.getLatestSerialNumber %}
|
{% if part.getLatestSerialNumber %}
|
||||||
{{ part.getLatestSerialNumber }}
|
{{ part.getLatestSerialNumber }}{% include "clip.html"%}
|
||||||
{% else %}
|
{% else %}
|
||||||
<em>{% trans "No serial numbers recorded" %}</em>
|
<em>{% trans "No serial numbers recorded" %}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info-circle'></span></td>
|
<td><span class='fas fa-info-circle'></span></td>
|
||||||
<td><strong>{% trans "Description" %}</strong></td>
|
<td><strong>{% trans "Description" %}</strong></td>
|
||||||
<td>{{ part.description }}</td>
|
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if part.variant_of %}
|
{% if part.variant_of %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -96,7 +96,7 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
<td><strong>{% trans "Default Supplier" %}</strong></td>
|
<td><strong>{% trans "Default Supplier" %}</strong></td>
|
||||||
<td><a href="{% url 'supplier-part-detail' part.default_supplier.id %}">
|
<td><a href="{% url 'supplier-part-detail' part.default_supplier.id %}">
|
||||||
{{ part.default_supplier.supplier.name }} | {{ part.default_supplier.SKU }}
|
{{ part.default_supplier.supplier.name }} | {{ part.default_supplier.SKU }}{% include "clip.html"%}
|
||||||
</a></td>
|
</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -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>
|
||||||
|
@ -9,19 +9,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>{% trans 'Quantity' %}</h4>
|
<h4>{% trans 'Quantity' %}</h4>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed table-price-two'>
|
||||||
<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>
|
||||||
@ -47,7 +48,7 @@
|
|||||||
|
|
||||||
{% 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>
|
||||||
@ -78,6 +79,20 @@
|
|||||||
</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 %}
|
||||||
<div class='alert alert-danger alert-block'>
|
<div class='alert alert-danger alert-block'>
|
||||||
|
@ -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,6 +200,16 @@ class I18nStaticNode(StaticNode):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# use the dynamic url - tag if in Debugging-Mode
|
||||||
|
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')
|
@register.tag('i18n_static')
|
||||||
def do_i18n_static(parser, token):
|
def do_i18n_static(parser, token):
|
||||||
"""
|
"""
|
||||||
|
@ -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'),
|
||||||
|
@ -886,7 +886,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
|
||||||
@ -1961,7 +1961,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):
|
||||||
@ -1971,12 +1970,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
|
||||||
|
|
||||||
@ -2077,12 +2071,22 @@ class PartPricing(AjaxView):
|
|||||||
ret.append(line)
|
ret.append(line)
|
||||||
|
|
||||||
ctx['price_history'] = ret
|
ctx['price_history'] = ret
|
||||||
|
# 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):
|
||||||
|
|
||||||
@ -2091,16 +2095,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,
|
||||||
|
@ -133,12 +133,15 @@
|
|||||||
<!-- boostrap-table-treegrid -->
|
<!-- boostrap-table-treegrid -->
|
||||||
<script type='text/javascript' src='{% static "bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js" %}'></script>
|
<script type='text/javascript' src='{% static "bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js" %}'></script>
|
||||||
|
|
||||||
|
<!-- 3rd party general js -->
|
||||||
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
|
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
|
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
|
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/chart.min.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/chart.min.js' %}"></script>
|
||||||
|
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- general InvenTree -->
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
||||||
|
5
InvenTree/templates/clip.html
Normal file
5
InvenTree/templates/clip.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<span class="float-right">
|
||||||
|
<button class="btn clip-btn" type="button" data-toggle='tooltip' title='{% trans "copy to clipboard" %}'><i class="fas fa-copy"></i></button>
|
||||||
|
</span>
|
@ -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>
|
||||||
|
4
tasks.py
4
tasks.py
@ -65,7 +65,7 @@ def manage(c, cmd, pty=False):
|
|||||||
cmd - django command to run
|
cmd - django command to run
|
||||||
"""
|
"""
|
||||||
|
|
||||||
c.run('cd {path} && python3 manage.py {cmd}'.format(
|
c.run('cd "{path}" && python3 manage.py {cmd}'.format(
|
||||||
path=managePyDir(),
|
path=managePyDir(),
|
||||||
cmd=cmd
|
cmd=cmd
|
||||||
), pty=pty)
|
), pty=pty)
|
||||||
@ -185,7 +185,7 @@ def translate(c):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Translate applicable .py / .html / .js files
|
# Translate applicable .py / .html / .js files
|
||||||
manage(c, "makemessages --all -e py,html,js")
|
manage(c, "makemessages --all -e py,html,js --no-wrap")
|
||||||
manage(c, "compilemessages")
|
manage(c, "compilemessages")
|
||||||
|
|
||||||
path = os.path.join('InvenTree', 'script', 'translation_stats.py')
|
path = os.path.join('InvenTree', 'script', 'translation_stats.py')
|
||||||
|
Loading…
Reference in New Issue
Block a user