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
75f487aee9
@ -357,6 +357,8 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
- Serial numbers must be positive
|
- Serial numbers must be positive
|
||||||
- Serial numbers can be split by whitespace / newline / commma chars
|
- Serial numbers can be split by whitespace / newline / commma chars
|
||||||
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
||||||
|
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
||||||
|
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
expected_quantity: The number of (unique) serial numbers we expect
|
expected_quantity: The number of (unique) serial numbers we expect
|
||||||
@ -369,6 +371,13 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
numbers = []
|
numbers = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
|
# helpers
|
||||||
|
def number_add(n):
|
||||||
|
if n in numbers:
|
||||||
|
errors.append(_('Duplicate serial: {n}').format(n=n))
|
||||||
|
else:
|
||||||
|
numbers.append(n)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
expected_quantity = int(expected_quantity)
|
expected_quantity = int(expected_quantity)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -395,10 +404,7 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
|
|
||||||
if a < b:
|
if a < b:
|
||||||
for n in range(a, b + 1):
|
for n in range(a, b + 1):
|
||||||
if n in numbers:
|
number_add(n)
|
||||||
errors.append(_('Duplicate serial: {n}').format(n=n))
|
|
||||||
else:
|
|
||||||
numbers.append(n)
|
|
||||||
else:
|
else:
|
||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
|
||||||
@ -409,6 +415,31 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# plus signals either
|
||||||
|
# 1: 'start+': expected number of serials, starting at start
|
||||||
|
# 2: 'start+number': number of serials, starting at start
|
||||||
|
elif '+' in group:
|
||||||
|
items = group.split('+')
|
||||||
|
|
||||||
|
# case 1, 2
|
||||||
|
if len(items) == 2:
|
||||||
|
start = int(items[0])
|
||||||
|
|
||||||
|
# case 2
|
||||||
|
if bool(items[1]):
|
||||||
|
end = start + int(items[1]) + 1
|
||||||
|
|
||||||
|
# case 1
|
||||||
|
else:
|
||||||
|
end = start + expected_quantity
|
||||||
|
|
||||||
|
for n in range(start, end):
|
||||||
|
number_add(n)
|
||||||
|
# no case
|
||||||
|
else:
|
||||||
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if group in numbers:
|
if group in numbers:
|
||||||
errors.append(_("Duplicate serial: {g}".format(g=group)))
|
errors.append(_("Duplicate serial: {g}".format(g=group)))
|
||||||
|
@ -491,7 +491,7 @@ LANGUAGES = [
|
|||||||
('en', _('English')),
|
('en', _('English')),
|
||||||
('fr', _('French')),
|
('fr', _('French')),
|
||||||
('de', _('German')),
|
('de', _('German')),
|
||||||
('pk', _('Polish')),
|
('pl', _('Polish')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -244,6 +244,14 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
self.assertIn(3, sn)
|
self.assertIn(3, sn)
|
||||||
self.assertIn(13, sn)
|
self.assertIn(13, sn)
|
||||||
|
|
||||||
|
sn = e("1+", 10)
|
||||||
|
self.assertEqual(len(sn), 10)
|
||||||
|
self.assertEqual(sn, [_ for _ in range(1, 11)])
|
||||||
|
|
||||||
|
sn = e("4, 1+2", 4)
|
||||||
|
self.assertEqual(len(sn), 4)
|
||||||
|
self.assertEqual(sn, ["4", 1, 2, 3])
|
||||||
|
|
||||||
def test_failures(self):
|
def test_failures(self):
|
||||||
|
|
||||||
e = helpers.extract_serial_numbers
|
e = helpers.extract_serial_numbers
|
||||||
|
@ -39,7 +39,7 @@ from rest_framework.documentation import include_docs_urls
|
|||||||
|
|
||||||
from .views import IndexView, SearchView, DatabaseStatsView
|
from .views import IndexView, SearchView, DatabaseStatsView
|
||||||
from .views import SettingsView, EditUserView, SetPasswordView
|
from .views import SettingsView, EditUserView, SetPasswordView
|
||||||
from .views import ColorThemeSelectView, SettingCategorySelectView
|
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||||
from .views import DynamicJsView
|
from .views import DynamicJsView
|
||||||
|
|
||||||
from common.views import SettingEdit
|
from common.views import SettingEdit
|
||||||
@ -79,7 +79,8 @@ apipatterns = [
|
|||||||
settings_urls = [
|
settings_urls = [
|
||||||
|
|
||||||
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
||||||
url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
|
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||||
|
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
||||||
|
|
||||||
url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
|
url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
|
||||||
url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
||||||
|
@ -769,12 +769,12 @@ class SettingsView(TemplateView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class ColorThemeSelectView(FormView):
|
class AppearanceSelectView(FormView):
|
||||||
""" View for selecting a color theme """
|
""" View for selecting a color theme """
|
||||||
|
|
||||||
form_class = ColorThemeSelectForm
|
form_class = ColorThemeSelectForm
|
||||||
success_url = reverse_lazy('settings-theme')
|
success_url = reverse_lazy('settings-appearance')
|
||||||
template_name = "InvenTree/settings/theme.html"
|
template_name = "InvenTree/settings/appearance.html"
|
||||||
|
|
||||||
def get_user_theme(self):
|
def get_user_theme(self):
|
||||||
""" Get current user color theme """
|
""" Get current user color theme """
|
||||||
@ -788,7 +788,7 @@ class ColorThemeSelectView(FormView):
|
|||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
""" Select current user color theme as initial choice """
|
""" Select current user color theme as initial choice """
|
||||||
|
|
||||||
initial = super(ColorThemeSelectView, self).get_initial()
|
initial = super(AppearanceSelectView, self).get_initial()
|
||||||
|
|
||||||
user_theme = self.get_user_theme()
|
user_theme = self.get_user_theme()
|
||||||
if user_theme:
|
if user_theme:
|
||||||
|
@ -996,14 +996,28 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def required_parts(self):
|
def required_parts(self):
|
||||||
""" Returns a dict of parts required to build this part (BOM) """
|
""" Returns a list of parts required to build this part (BOM) """
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
for item in self.part.bom_items.all().prefetch_related('sub_part'):
|
for item in self.bom_items:
|
||||||
parts.append(item.sub_part)
|
parts.append(item.sub_part)
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_parts_to_complete_build(self):
|
||||||
|
""" Returns a list of parts required to complete the full build """
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
for bom_item in self.bom_items:
|
||||||
|
# Get remaining quantity needed
|
||||||
|
required_quantity_to_complete_build = self.remaining * bom_item.quantity
|
||||||
|
# Compare to net stock
|
||||||
|
if bom_item.sub_part.net_stock < required_quantity_to_complete_build:
|
||||||
|
parts.append(bom_item.sub_part)
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
def availableStockItems(self, part, output):
|
def availableStockItems(self, part, output):
|
||||||
"""
|
"""
|
||||||
Returns stock items which are available for allocation to this build.
|
Returns stock items which are available for allocation to this build.
|
||||||
|
@ -40,8 +40,8 @@
|
|||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
{% trans "The following items will be created" %}
|
{% trans "The following items will be created" %}
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content' style='padding-bottom:16px'>
|
||||||
{% include "hover_image.html" with image=build.part.image hover=True %}
|
{% include "hover_image.html" with image=build.part.image %}
|
||||||
{% if output.serialized %}
|
{% if output.serialized %}
|
||||||
{{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
|
{{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -157,6 +157,17 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
quantity = form.cleaned_data.get('output_quantity', None)
|
quantity = form.cleaned_data.get('output_quantity', None)
|
||||||
serials = form.cleaned_data.get('serial_numbers', None)
|
serials = form.cleaned_data.get('serial_numbers', None)
|
||||||
|
|
||||||
|
if quantity:
|
||||||
|
build = self.get_object()
|
||||||
|
|
||||||
|
# Check that requested output don't exceed build remaining quantity
|
||||||
|
maximum_output = int(build.remaining - build.incomplete_count)
|
||||||
|
if quantity > maximum_output:
|
||||||
|
form.add_error(
|
||||||
|
'output_quantity',
|
||||||
|
_('Maximum output quantity is ') + str(maximum_output),
|
||||||
|
)
|
||||||
|
|
||||||
# Check that the serial numbers are valid
|
# Check that the serial numbers are valid
|
||||||
if serials:
|
if serials:
|
||||||
try:
|
try:
|
||||||
@ -212,7 +223,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
|
|
||||||
# Calculate the required quantity
|
# Calculate the required quantity
|
||||||
quantity = max(0, build.remaining - build.incomplete_count)
|
quantity = max(0, build.remaining - build.incomplete_count)
|
||||||
initials['output_quantity'] = quantity
|
initials['output_quantity'] = int(quantity)
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
|
|||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
# Filter by manufacturer
|
# Filter by manufacturer
|
||||||
manufacturer = params.get('company', None)
|
manufacturer = params.get('manufacturer', None)
|
||||||
|
|
||||||
if manufacturer is not None:
|
if manufacturer is not None:
|
||||||
queryset = queryset.filter(manufacturer=manufacturer)
|
queryset = queryset.filter(manufacturer=manufacturer)
|
||||||
|
@ -6,7 +6,7 @@ Company database model definitions
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import decimal
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@ -566,12 +566,15 @@ class SupplierPart(models.Model):
|
|||||||
- If order multiples are to be observed, then we need to calculate based on that, too
|
- If order multiples are to be observed, then we need to calculate based on that, too
|
||||||
"""
|
"""
|
||||||
|
|
||||||
price_breaks = self.price_breaks.filter(quantity__lte=quantity)
|
price_breaks = self.price_breaks.all()
|
||||||
|
|
||||||
# No price break information available?
|
# No price break information available?
|
||||||
if len(price_breaks) == 0:
|
if len(price_breaks) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Check if quantity is fraction and disable multiples
|
||||||
|
multiples = (quantity % 1 == 0)
|
||||||
|
|
||||||
# Order multiples
|
# Order multiples
|
||||||
if multiples:
|
if multiples:
|
||||||
quantity = int(math.ceil(quantity / self.multiple) * self.multiple)
|
quantity = int(math.ceil(quantity / self.multiple) * self.multiple)
|
||||||
@ -584,7 +587,12 @@ class SupplierPart(models.Model):
|
|||||||
# Default currency selection
|
# Default currency selection
|
||||||
currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||||
|
|
||||||
|
pb_min = None
|
||||||
for pb in self.price_breaks.all():
|
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)
|
# Ignore this pricebreak (quantity is too high)
|
||||||
if pb.quantity > quantity:
|
if pb.quantity > quantity:
|
||||||
continue
|
continue
|
||||||
@ -598,6 +606,17 @@ class SupplierPart(models.Model):
|
|||||||
# Convert everything to the selected currency
|
# Convert everything to the selected currency
|
||||||
pb_cost = pb.convert_to(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:
|
if pb_found:
|
||||||
cost = pb_cost * quantity
|
cost = pb_cost * quantity
|
||||||
return normalize(cost + self.base_cost)
|
return normalize(cost + self.base_cost)
|
||||||
@ -675,4 +694,4 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
|||||||
db_table = 'part_supplierpricebreak'
|
db_table = 'part_supplierpricebreak'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.part.MPN} - {self.price} @ {self.quantity}'
|
return f'{self.part.SKU} - {self.price} @ {self.quantity}'
|
||||||
|
@ -192,8 +192,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
manufacturer_id = self.initial_data.get('manufacturer', None)
|
manufacturer_id = self.initial_data.get('manufacturer', None)
|
||||||
MPN = self.initial_data.get('MPN', None)
|
MPN = self.initial_data.get('MPN', None)
|
||||||
|
|
||||||
if manufacturer_id or MPN:
|
if manufacturer_id and MPN:
|
||||||
kwargs = {'manufacturer': manufacturer_id,
|
kwargs = {
|
||||||
|
'manufacturer': manufacturer_id,
|
||||||
'MPN': MPN,
|
'MPN': MPN,
|
||||||
}
|
}
|
||||||
supplier_part.save(**kwargs)
|
supplier_part.save(**kwargs)
|
||||||
|
@ -100,7 +100,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(response.data['MPN'], 'MPN_TEST')
|
self.assertEqual(response.data['MPN'], 'MPN_TEST')
|
||||||
|
|
||||||
# Filter by manufacturer
|
# Filter by manufacturer
|
||||||
data = {'company': 7}
|
data = {'manufacturer': 7}
|
||||||
response = self.get(url, data)
|
response = self.get(url, data)
|
||||||
self.assertEqual(len(response.data), 3)
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from django.test import TestCase
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from .models import Company, Contact, ManufacturerPart, SupplierPart
|
from .models import Company, Contact, ManufacturerPart, SupplierPart
|
||||||
from .models import rename_company_image
|
from .models import rename_company_image
|
||||||
@ -103,8 +104,9 @@ class CompanySimpleTest(TestCase):
|
|||||||
self.assertEqual(p(100), 350)
|
self.assertEqual(p(100), 350)
|
||||||
|
|
||||||
p = self.acme0002.get_price
|
p = self.acme0002.get_price
|
||||||
self.assertEqual(p(1), None)
|
self.assertEqual(p(0.5), 3.5)
|
||||||
self.assertEqual(p(2), None)
|
self.assertEqual(p(1), 7)
|
||||||
|
self.assertEqual(p(2), 14)
|
||||||
self.assertEqual(p(5), 35)
|
self.assertEqual(p(5), 35)
|
||||||
self.assertEqual(p(45), 315)
|
self.assertEqual(p(45), 315)
|
||||||
self.assertEqual(p(55), 68.75)
|
self.assertEqual(p(55), 68.75)
|
||||||
@ -112,6 +114,7 @@ class CompanySimpleTest(TestCase):
|
|||||||
def test_part_pricing(self):
|
def test_part_pricing(self):
|
||||||
m2x4 = Part.objects.get(name='M2x4 LPHS')
|
m2x4 = Part.objects.get(name='M2x4 LPHS')
|
||||||
|
|
||||||
|
self.assertEqual(m2x4.get_price_info(5.5), "38.5 - 41.25")
|
||||||
self.assertEqual(m2x4.get_price_info(10), "70 - 75")
|
self.assertEqual(m2x4.get_price_info(10), "70 - 75")
|
||||||
self.assertEqual(m2x4.get_price_info(100), "125 - 350")
|
self.assertEqual(m2x4.get_price_info(100), "125 - 350")
|
||||||
|
|
||||||
@ -121,7 +124,8 @@ class CompanySimpleTest(TestCase):
|
|||||||
|
|
||||||
m3x12 = Part.objects.get(name='M3x12 SHCS')
|
m3x12 = Part.objects.get(name='M3x12 SHCS')
|
||||||
|
|
||||||
self.assertIsNone(m3x12.get_price_info(3))
|
self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4'))
|
||||||
|
self.assertEqual(m3x12.get_price_info(3), Decimal('24'))
|
||||||
self.assertIsNotNone(m3x12.get_price_info(50))
|
self.assertIsNotNone(m3x12.get_price_info(50))
|
||||||
|
|
||||||
def test_currency_validation(self):
|
def test_currency_validation(self):
|
||||||
|
@ -344,14 +344,16 @@ class PurchaseOrder(Order):
|
|||||||
raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")})
|
raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not (quantity % 1 == 0):
|
||||||
|
raise ValidationError({"quantity": _("Quantity must be an integer")})
|
||||||
|
if quantity < 0:
|
||||||
|
raise ValidationError({"quantity": _("Quantity must be a positive number")})
|
||||||
quantity = int(quantity)
|
quantity = int(quantity)
|
||||||
if quantity <= 0:
|
except (ValueError, TypeError):
|
||||||
raise ValidationError({"quantity": _("Quantity must be greater than zero")})
|
|
||||||
except ValueError:
|
|
||||||
raise ValidationError({"quantity": _("Invalid quantity provided")})
|
raise ValidationError({"quantity": _("Invalid quantity provided")})
|
||||||
|
|
||||||
# Create a new stock item
|
# Create a new stock item
|
||||||
if line.part:
|
if line.part and quantity > 0:
|
||||||
stock = stock_models.StockItem(
|
stock = stock_models.StockItem(
|
||||||
part=line.part.part,
|
part=line.part.part,
|
||||||
supplier_part=line.part,
|
supplier_part=line.part,
|
||||||
|
@ -171,11 +171,35 @@ $("#edit-order").click(function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#receive-order").click(function() {
|
||||||
|
launchModalForm("{% url 'po-receive' order.id %}", {
|
||||||
|
reload: true,
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
field: 'location',
|
||||||
|
label: '{% trans "New Location" %}',
|
||||||
|
title: '{% trans "Create new stock location" %}',
|
||||||
|
url: "{% url 'stock-location-create' %}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#complete-order").click(function() {
|
||||||
|
launchModalForm("{% url 'po-complete' order.id %}", {
|
||||||
|
reload: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$("#cancel-order").click(function() {
|
$("#cancel-order").click(function() {
|
||||||
launchModalForm("{% url 'po-cancel' order.id %}", {
|
launchModalForm("{% url 'po-cancel' order.id %}", {
|
||||||
reload: true,
|
reload: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#export-order").click(function() {
|
||||||
|
location.href = "{% url 'po-export' order.id %}";
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
{% trans "Cancelling this order means that the order will no longer be editable." %}
|
<div class='alert alert-danger alert-block'>
|
||||||
|
{% trans "Cancelling this order means that the order and line items will no longer be editable." %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
{% trans 'Mark this order as complete?' %}
|
{% trans 'Mark this order as complete?' %}
|
||||||
{% if not order.is_complete %}
|
{% if not order.is_complete %}
|
||||||
<div class='alert alert-warning alert-block'>
|
<div class='alert alert-warning alert-block' style='margin-top:12px'>
|
||||||
{% trans 'This order has line items which have not been marked as received.' %}
|
{% trans 'This order has line items which have not been marked as received.' %}</br>
|
||||||
{% trans 'Marking this order as complete will remove these line items.' %}
|
{% trans 'Completing this order means that the order and line items will no longer be editable.' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
|
<div class='alert alert-warning alert-block'>
|
||||||
{% trans 'After placing this purchase order, line items will no longer be editable.' %}
|
{% trans 'After placing this purchase order, line items will no longer be editable.' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -35,31 +35,6 @@
|
|||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
|
||||||
$("#receive-order").click(function() {
|
|
||||||
launchModalForm("{% url 'po-receive' order.id %}", {
|
|
||||||
reload: true,
|
|
||||||
secondary: [
|
|
||||||
{
|
|
||||||
field: 'location',
|
|
||||||
label: '{% trans "New Location" %}',
|
|
||||||
title: '{% trans "Create new stock location" %}',
|
|
||||||
url: "{% url 'stock-location-create' %}",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#complete-order").click(function() {
|
|
||||||
launchModalForm("{% url 'po-complete' order.id %}", {
|
|
||||||
reload: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#export-order").click(function() {
|
|
||||||
location.href = "{% url 'po-export' order.id %}";
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||||
$('#new-po-line').click(function() {
|
$('#new-po-line').click(function() {
|
||||||
launchModalForm("{% url 'po-line-item-create' %}",
|
launchModalForm("{% url 'po-line-item-create' %}",
|
||||||
@ -261,5 +236,4 @@ $("#po-table").inventreeTable({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db.models import Q, F, Count, Prefetch, Sum
|
from django.db.models import Q, F, Count
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -635,29 +635,15 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
# TODO: Need to figure out a cheaper way of making this filter query
|
# TODO: Need to figure out a cheaper way of making this filter query
|
||||||
|
|
||||||
if stock_to_build is not None:
|
if stock_to_build is not None:
|
||||||
# Filter only active parts
|
# Get active builds
|
||||||
queryset = queryset.filter(active=True)
|
builds = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||||
# Prefetch current active builds
|
|
||||||
build_active_queryset = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
|
|
||||||
build_active_prefetch = Prefetch('builds',
|
|
||||||
queryset=build_active_queryset,
|
|
||||||
to_attr='current_builds')
|
|
||||||
parts = queryset.prefetch_related(build_active_prefetch)
|
|
||||||
|
|
||||||
# Store parts with builds needing stock
|
# Store parts with builds needing stock
|
||||||
parts_need_stock = []
|
parts_needed_to_complete_builds = []
|
||||||
|
# Filter required parts
|
||||||
|
for build in builds:
|
||||||
|
parts_needed_to_complete_builds += [part.pk for part in build.required_parts_to_complete_build]
|
||||||
|
|
||||||
# Find parts with active builds
|
queryset = queryset.filter(pk__in=parts_needed_to_complete_builds)
|
||||||
# where any subpart's stock is lower than quantity being built
|
|
||||||
for part in parts:
|
|
||||||
if part.current_builds:
|
|
||||||
builds_ids = [build.id for build in part.current_builds]
|
|
||||||
total_build_quantity = build_active_queryset.filter(pk__in=builds_ids).aggregate(quantity=Sum('quantity'))['quantity']
|
|
||||||
|
|
||||||
if part.can_build < total_build_quantity:
|
|
||||||
parts_need_stock.append(part.pk)
|
|
||||||
|
|
||||||
queryset = queryset.filter(pk__in=parts_need_stock)
|
|
||||||
|
|
||||||
# Optionally limit the maximum number of returned results
|
# Optionally limit the maximum number of returned results
|
||||||
# e.g. for displaying "recent part" list
|
# e.g. for displaying "recent part" list
|
||||||
|
@ -116,6 +116,12 @@ def inventree_docs_url(*args, **kwargs):
|
|||||||
return "https://inventree.readthedocs.io/"
|
return "https://inventree.readthedocs.io/"
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def inventree_credits_url(*args, **kwargs):
|
||||||
|
""" Return URL for InvenTree credits site """
|
||||||
|
return "https://inventree.readthedocs.io/en/latest/credits/"
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def setting_object(key, *args, **kwargs):
|
def setting_object(key, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -1959,6 +1959,11 @@ class PartPricing(AjaxView):
|
|||||||
|
|
||||||
role_required = ['sales_order.view', 'part.view']
|
role_required = ['sales_order.view', 'part.view']
|
||||||
|
|
||||||
|
def get_quantity(self):
|
||||||
|
""" Return set quantity in decimal format """
|
||||||
|
|
||||||
|
return Decimal(self.request.POST.get('quantity', 1))
|
||||||
|
|
||||||
def get_part(self):
|
def get_part(self):
|
||||||
try:
|
try:
|
||||||
return Part.objects.get(id=self.kwargs['pk'])
|
return Part.objects.get(id=self.kwargs['pk'])
|
||||||
@ -1967,12 +1972,12 @@ class PartPricing(AjaxView):
|
|||||||
|
|
||||||
def get_pricing(self, quantity=1, currency=None):
|
def get_pricing(self, quantity=1, currency=None):
|
||||||
|
|
||||||
try:
|
# try:
|
||||||
quantity = int(quantity)
|
# quantity = int(quantity)
|
||||||
except ValueError:
|
# except ValueError:
|
||||||
quantity = 1
|
# quantity = 1
|
||||||
|
|
||||||
if quantity < 1:
|
if quantity <= 0:
|
||||||
quantity = 1
|
quantity = 1
|
||||||
|
|
||||||
# TODO - Capacity for price comparison in different currencies
|
# TODO - Capacity for price comparison in different currencies
|
||||||
@ -2002,16 +2007,19 @@ class PartPricing(AjaxView):
|
|||||||
min_buy_price /= scaler
|
min_buy_price /= scaler
|
||||||
max_buy_price /= scaler
|
max_buy_price /= scaler
|
||||||
|
|
||||||
|
min_unit_buy_price = round(min_buy_price / quantity, 3)
|
||||||
|
max_unit_buy_price = round(max_buy_price / quantity, 3)
|
||||||
|
|
||||||
min_buy_price = round(min_buy_price, 3)
|
min_buy_price = round(min_buy_price, 3)
|
||||||
max_buy_price = round(max_buy_price, 3)
|
max_buy_price = round(max_buy_price, 3)
|
||||||
|
|
||||||
if min_buy_price:
|
if min_buy_price:
|
||||||
ctx['min_total_buy_price'] = min_buy_price
|
ctx['min_total_buy_price'] = min_buy_price
|
||||||
ctx['min_unit_buy_price'] = min_buy_price / quantity
|
ctx['min_unit_buy_price'] = min_unit_buy_price
|
||||||
|
|
||||||
if max_buy_price:
|
if max_buy_price:
|
||||||
ctx['max_total_buy_price'] = max_buy_price
|
ctx['max_total_buy_price'] = max_buy_price
|
||||||
ctx['max_unit_buy_price'] = max_buy_price / quantity
|
ctx['max_unit_buy_price'] = max_unit_buy_price
|
||||||
|
|
||||||
# BOM pricing information
|
# BOM pricing information
|
||||||
if part.bom_count > 0:
|
if part.bom_count > 0:
|
||||||
@ -2024,16 +2032,19 @@ class PartPricing(AjaxView):
|
|||||||
min_bom_price /= scaler
|
min_bom_price /= scaler
|
||||||
max_bom_price /= scaler
|
max_bom_price /= scaler
|
||||||
|
|
||||||
|
min_unit_bom_price = round(min_bom_price / quantity, 3)
|
||||||
|
max_unit_bom_price = round(max_bom_price / quantity, 3)
|
||||||
|
|
||||||
min_bom_price = round(min_bom_price, 3)
|
min_bom_price = round(min_bom_price, 3)
|
||||||
max_bom_price = round(max_bom_price, 3)
|
max_bom_price = round(max_bom_price, 3)
|
||||||
|
|
||||||
if min_bom_price:
|
if min_bom_price:
|
||||||
ctx['min_total_bom_price'] = min_bom_price
|
ctx['min_total_bom_price'] = min_bom_price
|
||||||
ctx['min_unit_bom_price'] = min_bom_price / quantity
|
ctx['min_unit_bom_price'] = min_unit_bom_price
|
||||||
|
|
||||||
if max_bom_price:
|
if max_bom_price:
|
||||||
ctx['max_total_bom_price'] = max_bom_price
|
ctx['max_total_bom_price'] = max_bom_price
|
||||||
ctx['max_unit_bom_price'] = max_bom_price / quantity
|
ctx['max_unit_bom_price'] = max_unit_bom_price
|
||||||
|
|
||||||
# Stock history
|
# Stock history
|
||||||
if part_settings.part_show_graph and part.total_stock > 1:
|
if part_settings.part_show_graph and part.total_stock > 1:
|
||||||
@ -2077,10 +2088,11 @@ class PartPricing(AjaxView):
|
|||||||
|
|
||||||
currency = None
|
currency = None
|
||||||
|
|
||||||
try:
|
quantity = self.get_quantity()
|
||||||
quantity = int(self.request.POST.get('quantity', 1))
|
|
||||||
except ValueError:
|
# Retain quantity value set by user
|
||||||
quantity = 1
|
form = self.form_class()
|
||||||
|
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
|
||||||
@ -2090,7 +2102,7 @@ class PartPricing(AjaxView):
|
|||||||
'form_valid': False,
|
'form_valid': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.renderJsonResponse(request, self.form_class(), data=data, context=self.get_pricing(quantity, currency))
|
return self.renderJsonResponse(request, form, data=data, context=self.get_pricing(quantity, currency))
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplateCreate(AjaxCreateView):
|
class PartParameterTemplateCreate(AjaxCreateView):
|
||||||
|
67
InvenTree/templates/InvenTree/settings/appearance.html
Normal file
67
InvenTree/templates/InvenTree/settings/appearance.html
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{% extends "InvenTree/settings/settings.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block tabs %}
|
||||||
|
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block subtitle %}
|
||||||
|
{% trans "Theme Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block settings %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<h4>{% trans "Color Themes" %}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{% url 'settings-appearance' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% crispy form %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if invalid_color_theme %}
|
||||||
|
<div class="alert alert-danger alert-block" role="alert" style="display: inline-block;">
|
||||||
|
{% blocktrans %}
|
||||||
|
The CSS sheet "{{invalid_color_theme}}.css" for the currently selected color theme was not found.<br>
|
||||||
|
Please select another color theme :)
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<h4>{% trans "Language" %}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
|
||||||
|
<input name="next" type="hidden" value="{% url 'settings-appearance' %}">
|
||||||
|
<div class="col-sm-6" style="width: 200px;"><div id="div_id_name" class="form-group"><div class="controls ">
|
||||||
|
<select name="language" class="select form-control">
|
||||||
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
|
{% for language in languages %}
|
||||||
|
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
|
||||||
|
{{ language.name_local }} ({{ language.code }})
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div></div></div>
|
||||||
|
<div class="col-sm-6" style="width: auto;">
|
||||||
|
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -6,7 +6,7 @@
|
|||||||
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> {% trans "Account" %}</a>
|
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> {% trans "Account" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
||||||
<a href="{% url 'settings-theme' %}"><span class='fas fa-fill'></span> {% trans "Theme" %}</a>
|
<a href="{% url 'settings-appearance' %}"><span class='fas fa-fill'></span> {% trans "Appearance" %}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
{% extends "InvenTree/settings/settings.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load inventree_extras %}
|
|
||||||
|
|
||||||
{% block tabs %}
|
|
||||||
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block subtitle %}
|
|
||||||
{% trans "Theme Settings" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block settings %}
|
|
||||||
|
|
||||||
<div class='row'>
|
|
||||||
<div class='col-sm-6'>
|
|
||||||
<h4>{% trans "Color Themes" %}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form action="{% url 'settings-theme' %}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% crispy form %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% if invalid_color_theme %}
|
|
||||||
<div class="alert alert-danger alert-block" role="alert" style="display: inline-block;">
|
|
||||||
{% blocktrans %}
|
|
||||||
The CSS sheet "{{invalid_color_theme}}.css" for the currently selected color theme was not found.<br>
|
|
||||||
Please select another color theme :)
|
|
||||||
{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -58,6 +58,11 @@
|
|||||||
<td>{% trans "View Code on GitHub" %}</td>
|
<td>{% trans "View Code on GitHub" %}</td>
|
||||||
<td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
|
<td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-balance-scale'></span></td>
|
||||||
|
<td>{% trans "Credits" %}</td>
|
||||||
|
<td><a href="{% inventree_credits_url %}">{% inventree_credits_url %}</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-mobile-alt'></span></td>
|
<td><span class='fas fa-mobile-alt'></span></td>
|
||||||
<td>{% trans "Mobile App" %}</td>
|
<td>{% trans "Mobile App" %}</td>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
|
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
|
||||||
{% if roles.stock.add %}
|
{% if not read_only and roles.stock.add %}
|
||||||
<button class="btn btn-success" id='item-create' title='{% trans "New Stock Item" %}'>
|
<button class="btn btn-success" id='item-create' title='{% trans "New Stock Item" %}'>
|
||||||
<span class='fas fa-plus-circle'></span>
|
<span class='fas fa-plus-circle'></span>
|
||||||
</button>
|
</button>
|
||||||
@ -44,6 +44,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% if not read_only %}
|
||||||
{% if roles.stock.change or roles.stock.delete %}
|
{% if roles.stock.change or roles.stock.delete %}
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" title='{% trans "Stock Options" %}'>
|
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" title='{% trans "Stock Options" %}'>
|
||||||
@ -65,6 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<div class='filter-list' id='filter-list-stock'>
|
<div class='filter-list' id='filter-list-stock'>
|
||||||
<!-- An empty div in which the filter list will be constructed -->
|
<!-- An empty div in which the filter list will be constructed -->
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,11 +51,15 @@ To contribute to the translation effort, navigate to the [InvenTree crowdin proj
|
|||||||
|
|
||||||
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).
|
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).
|
||||||
|
|
||||||
## Getting Started
|
# Getting Started
|
||||||
|
|
||||||
Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start/install/) for installation and setup instructions.
|
Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start/install/) for installation and setup instructions.
|
||||||
|
|
||||||
## Integration
|
# Credits
|
||||||
|
|
||||||
|
The credits for all used packages are part of the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/credits/).
|
||||||
|
|
||||||
|
# Integration
|
||||||
|
|
||||||
InvenTree is designed to be extensible, and provides multiple options for integration with external applications or addition of custom plugins:
|
InvenTree is designed to be extensible, and provides multiple options for integration with external applications or addition of custom plugins:
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user