Merge branch 'master' of github.com:inventree/InvenTree into multi_part_forms

This commit is contained in:
eeintech 2021-05-11 10:22:34 -04:00
commit 10eb69caf9
166 changed files with 776 additions and 727 deletions

View File

@ -8,7 +8,7 @@ on:
- 'master'
jobs:
docker:
runs-on: ubuntu-latest

View File

@ -33,7 +33,7 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
ports:
- 3306:3306
steps:
- name: Checkout Code
uses: actions/checkout@v2

View File

@ -5,7 +5,7 @@ name: PostgreSQL
on: ["push", "pull_request"]
jobs:
test:
runs-on: ubuntu-latest

View File

@ -83,7 +83,7 @@ class InvenTreeAPITestCase(APITestCase):
self.assertEqual(response.status_code, code)
return response
def post(self, url, data):
"""
Issue a POST request

View File

@ -71,7 +71,7 @@ def status_codes(request):
def user_roles(request):
"""
Return a map of the current roles assigned to the user.
Roles are denoted by their simple names, and then the permission type.
Permissions can be access as follows:

View File

@ -17,5 +17,5 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
"""
Do not get any rates...
"""
return {}

View File

@ -102,5 +102,5 @@ class RoundingDecimalField(models.DecimalField):
}
defaults.update(kwargs)
return super().formfield(**kwargs)

View File

@ -35,7 +35,7 @@ def generateTestKey(test_name):
"""
Generate a test 'key' for a given test name.
This must not have illegal chars as it will be used for dict lookup in a template.
Tests must be named such that they will have unique keys.
"""
@ -102,7 +102,7 @@ def TestIfImageURL(url):
'.tif', '.tiff',
'.webp', '.gif',
]
def str2bool(text, test=True):
""" Test if a string 'looks' like a boolean value.
@ -137,10 +137,10 @@ def isNull(text):
"""
Test if a string 'looks' like a null value.
This is useful for querying the API against a null key.
Args:
text: Input text
Returns:
True if the text looks like a null value
"""
@ -157,7 +157,7 @@ def normalize(d):
d = Decimal(d)
d = d.normalize()
# Ref: https://docs.python.org/3/library/decimal.html
return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
@ -165,14 +165,14 @@ def normalize(d):
def increment(n):
"""
Attempt to increment an integer (or a string that looks like an integer!)
e.g.
001 -> 002
2 -> 3
AB01 -> AB02
QQQ -> QQQ
"""
value = str(n).strip()
@ -314,7 +314,7 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs):
def GetExportFormats():
""" Return a list of allowable file formats for exporting data """
return [
'csv',
'tsv',
@ -327,7 +327,7 @@ def GetExportFormats():
def DownloadFile(data, filename, content_type='application/text'):
""" Create a dynamic file for the user to download.
Args:
data: Raw file data (string or bytes)
filename: Filename for the file download
@ -525,7 +525,7 @@ def addUserPermission(user, permission):
"""
Shortcut function for adding a certain permission to a user.
"""
perm = Permission.objects.get(codename=permission)
user.user_permissions.add(perm)
@ -576,7 +576,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True):
continue
num = int(f.split('_')[0])
if oldest_file is None or num < oldest_num:
oldest_num = num
oldest_file = f
@ -585,7 +585,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True):
oldest_file = oldest_file.replace('.py', '')
return oldest_file
def getNewestMigrationFile(app, exclude_extension=True):
"""

View File

@ -77,12 +77,20 @@ class AuthRequiredMiddleware(object):
if request.path_info == reverse_lazy('logout'):
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
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
# the view is called.

View File

@ -129,7 +129,7 @@ class InvenTreeTree(MPTTModel):
Here an 'item' is considered to be the 'leaf' at the end of each branch,
and the exact nature here will depend on the class implementation.
The default implementation returns zero
"""
return 0

View File

@ -17,7 +17,7 @@ class RolePermission(permissions.BasePermission):
- PUT
- PATCH
- DELETE
Specify the required "role" using the role_required attribute.
e.g.

View File

@ -44,7 +44,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
In addition to running validators on the serializer fields,
this class ensures that the underlying model is also validated.
"""
# Run any native validation checks first (may throw an ValidationError)
data = super(serializers.ModelSerializer, self).validate(data)

View File

@ -507,7 +507,7 @@
padding-right: 6px;
padding-top: 3px;
padding-bottom: 2px;
};
}
.panel-heading .badge {
float: right;
@ -568,7 +568,7 @@
}
.media {
//padding-top: 15px;
/* padding-top: 15px; */
overflow: visible;
}
@ -594,8 +594,8 @@
width: 160px;
position: fixed;
z-index: 1;
//top: 0;
//left: 0;
/* top: 0;
left: 0; */
overflow-x: hidden;
padding-top: 20px;
padding-right: 25px;
@ -826,7 +826,7 @@ input[type="submit"] {
width: 100%;
padding: 20px;
z-index: 5000;
pointer-events: none; // Prevent this div from blocking links underneath
pointer-events: none; /* Prevent this div from blocking links underneath */
}
.alert {
@ -936,4 +936,15 @@ input[type="submit"] {
input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control {
line-height: unset;
}
}
.clip-btn {
font-size: 10px;
padding: 0px 6px;
color: var(--label-grey);
background: none;
}
.clip-btn:hover {
background: var(--label-grey);
}

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,14 @@
function attachClipboard(selector) {
new ClipboardJS(selector, {
text: function(trigger) {
var content = trigger.parentElement.parentElement.textContent;
return content.trim();
}
});
}
function inventreeDocReady() {
/* Run this function when the HTML document is loaded.
* This will be called for every page that extends "base.html"
@ -48,6 +59,10 @@ function inventreeDocReady() {
no_post: true,
});
});
// Initialize clipboard-buttons
attachClipboard('.clip-btn');
}
function isFileTransfer(transfer) {

View File

@ -64,7 +64,7 @@ def is_email_configured():
if not settings.EMAIL_HOST_USER:
configured = False
# Display warning unless in test mode
if not settings.TESTING:
logger.warning("EMAIL_HOST_USER is not configured")

View File

@ -16,7 +16,7 @@ class StatusCode:
# If the key cannot be found, pass it back
if key not in cls.options.keys():
return key
value = cls.options.get(key, key)
color = cls.colors.get(key, 'grey')

View File

@ -119,7 +119,7 @@ class APITests(InvenTreeAPITestCase):
self.assertNotIn('add', roles[rule])
self.assertNotIn('change', roles[rule])
self.assertNotIn('delete', roles[rule])
def test_with_superuser(self):
"""
Superuser should have *all* roles assigned

View File

@ -37,7 +37,7 @@ class ScheduledTaskTests(TestCase):
# Attempt to schedule the same task again
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5)
self.assertEqual(self.get_tasks(task).count(), 1)
# But the 'minutes' should have been updated
t = Schedule.objects.get(func=task)
self.assertEqual(t.minutes, 5)

View File

@ -97,7 +97,7 @@ class TestHelpers(TestCase):
self.assertEqual(helpers.getMediaUrl('xx/yy.png'), '/media/xx/yy.png')
def testDecimal2String(self):
self.assertEqual(helpers.decimal2string(Decimal('1.2345000')), '1.2345')
self.assertEqual(helpers.decimal2string('test'), 'test')
@ -205,7 +205,7 @@ class TestMPTT(TestCase):
child = StockLocation.objects.get(pk=5)
parent.parent = child
with self.assertRaises(InvalidMove):
parent.save()
@ -223,7 +223,7 @@ class TestMPTT(TestCase):
drawer.save()
self.assertNotEqual(tree, drawer.tree_id)
class TestSerialNumberExtraction(TestCase):
""" Tests for serial number extraction code """

View File

@ -81,7 +81,7 @@ settings_urls = [
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
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'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'),
@ -137,7 +137,7 @@ urlpatterns = [
url(r'^login/?', auth_views.LoginView.as_view(), name='login'),
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logged_out.html'), name='logout'),
url(r'^settings/', include(settings_urls)),
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),

View File

@ -130,7 +130,7 @@ def validate_overage(value):
if i < 0:
raise ValidationError(_("Overage value must not be negative"))
# Looks like an integer!
return True
except ValueError:

View File

@ -176,7 +176,7 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
if role not in RuleSet.RULESET_NAMES:
raise ValueError(f"Role '{role}' is not a valid role")
if permission not in RuleSet.RULESET_PERMISSIONS:
raise ValueError(f"Permission '{permission}' is not a valid permission")
@ -223,7 +223,7 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
Return the 'permission_class' required for the current View.
Must be one of:
- view
- change
- add
@ -389,7 +389,7 @@ class QRCodeView(AjaxView):
"""
ajax_template_name = "qr_code.html"
def get(self, request, *args, **kwargs):
self.request = request
self.pk = self.kwargs['pk']
@ -398,7 +398,7 @@ class QRCodeView(AjaxView):
def get_qr_data(self):
""" Returns the text object to render to a QR code.
The actual rendering will be handled by the template """
return None
def get_context_data(self):
@ -406,7 +406,7 @@ class QRCodeView(AjaxView):
Explicity passes the parameter 'qr_data'
"""
context = {}
qr = self.get_qr_data()
@ -415,7 +415,7 @@ class QRCodeView(AjaxView):
context['qr_data'] = qr
else:
context['error_msg'] = 'Error generating QR code'
return context
@ -507,7 +507,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
"""
super(UpdateView, self).get(request, *args, **kwargs)
return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data())
def save(self, object, form, **kwargs):
@ -673,7 +673,7 @@ class SetPasswordView(AjaxUpdateView):
p1 = request.POST.get('enter_password', '')
p2 = request.POST.get('confirm_password', '')
if valid:
# Passwords must match
@ -712,7 +712,7 @@ class IndexView(TemplateView):
# Generate a list of orderable parts which have stock below their minimum values
# TODO - Is there a less expensive way to get these from the database
# context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()]
# Generate a list of assembly parts which have stock below their minimum values
# TODO - Is there a less expensive way to get these from the database
# context['to_build'] = [part for part in Part.objects.filter(assembly=True) if part.need_to_restock()]
@ -752,7 +752,7 @@ class DynamicJsView(TemplateView):
template_name = ""
content_type = 'text/javascript'
class SettingsView(TemplateView):
""" View for configuring User settings
@ -830,7 +830,7 @@ class AppearanceSelectView(FormView):
if form.is_valid():
theme_selected = form.cleaned_data['name']
# Set color theme to form selection
user_theme.name = theme_selected
user_theme.save()
@ -893,7 +893,7 @@ class DatabaseStatsView(AjaxView):
# Part stats
ctx['part_count'] = Part.objects.count()
ctx['part_cat_count'] = PartCategory.objects.count()
# Stock stats
ctx['stock_item_count'] = StockItem.objects.count()
ctx['stock_loc_count'] = StockLocation.objects.count()

View File

@ -73,7 +73,7 @@ class BarcodeScan(APIView):
# A plugin has been found!
if plugin is not None:
# Try to associate with a stock item
item = plugin.getStockItem()
@ -133,7 +133,7 @@ class BarcodeScan(APIView):
class BarcodeAssign(APIView):
"""
Endpoint for assigning a barcode to a stock item.
- This only works if the barcode is not already associated with an object in the database
- If the barcode does not match an object, then the barcode hash is assigned to the StockItem
"""
@ -178,7 +178,7 @@ class BarcodeAssign(APIView):
# Matching plugin was found
if plugin is not None:
hash = plugin.hash()
response['hash'] = hash
response['plugin'] = plugin.name
@ -234,7 +234,7 @@ class BarcodeAssign(APIView):
barcode_api_urls = [
url(r'^link/$', BarcodeAssign.as_view(), name='api-barcode-link'),
# Catch-all performs barcode 'scan'
url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
]

View File

@ -21,7 +21,7 @@ def hash_barcode(barcode_data):
HACK: Remove any 'non printable' characters from the hash,
as it seems browers will remove special control characters...
TODO: Work out a way around this!
"""

View File

@ -92,7 +92,7 @@ class BarcodeAPITest(APITestCase):
data = response.data
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('stockitem', data)
pk = data['stockitem']['pk']
@ -121,7 +121,7 @@ class BarcodeAPITest(APITestCase):
data = response.data
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('success', data)
hash = data['hash']

View File

@ -20,7 +20,7 @@ from .serializers import BuildSerializer, BuildItemSerializer
class BuildList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of Build objects.
- GET: Return list of objects (with filters)
- POST: Create a new Build object
"""
@ -65,7 +65,7 @@ class BuildList(generics.ListCreateAPIView):
queryset = BuildSerializer.annotate_queryset(queryset)
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)

View File

@ -118,7 +118,7 @@ class Build(MPTTModel):
def get_absolute_url(self):
return reverse('build-detail', kwargs={'pk': self.id})
reference = models.CharField(
unique=True,
max_length=64,
@ -168,7 +168,7 @@ class Build(MPTTModel):
null=True, blank=True,
help_text=_('SalesOrder to which this build is allocated')
)
take_from = models.ForeignKey(
'stock.StockLocation',
verbose_name=_('Source Location'),
@ -177,7 +177,7 @@ class Build(MPTTModel):
null=True, blank=True,
help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)')
)
destination = models.ForeignKey(
'stock.StockLocation',
verbose_name=_('Destination Location'),
@ -207,7 +207,7 @@ class Build(MPTTModel):
validators=[MinValueValidator(0)],
help_text=_('Build status code')
)
batch = models.CharField(
verbose_name=_('Batch Code'),
max_length=100,
@ -215,9 +215,9 @@ class Build(MPTTModel):
null=True,
help_text=_('Batch code for this build output')
)
creation_date = models.DateField(auto_now_add=True, editable=False, verbose_name=_('Creation Date'))
target_date = models.DateField(
null=True, blank=True,
verbose_name=_('Target completion date'),
@ -251,7 +251,7 @@ class Build(MPTTModel):
help_text=_('User responsible for this build order'),
related_name='builds_responsible',
)
link = InvenTree.fields.InvenTreeURLField(
verbose_name=_('External Link'),
blank=True, help_text=_('Link to external URL')
@ -272,7 +272,7 @@ class Build(MPTTModel):
else:
descendants = self.get_descendants(include_self=True)
Build.objects.filter(parent__pk__in=[d.pk for d in descendants])
def sub_build_count(self, cascade=True):
"""
Return the number of sub builds under this one.
@ -295,7 +295,7 @@ class Build(MPTTModel):
query = query.filter(Build.OVERDUE_FILTER)
return query.exists()
@property
def active(self):
"""
@ -441,7 +441,7 @@ class Build(MPTTModel):
# Extract the "most recent" build order reference
builds = cls.objects.exclude(reference=None)
if not builds.exists():
return None
@ -543,7 +543,7 @@ class Build(MPTTModel):
- The sub_item in the BOM line must *not* be trackable
- There is only a single stock item available (which has not already been allocated to this build)
- The stock item has an availability greater than zero
Returns:
A list object containing the StockItem objects to be allocated (and the quantities).
Each item in the list is a dict as follows:
@ -648,7 +648,7 @@ class Build(MPTTModel):
"""
Deletes all stock allocations for this build.
"""
allocations = BuildItem.objects.filter(build=self)
allocations.delete()
@ -1145,7 +1145,7 @@ class BuildItem(models.Model):
"""
self.validate_unique()
super().clean()
errors = {}
@ -1159,7 +1159,7 @@ class BuildItem(models.Model):
# Allocated part must be in the BOM for the master part
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
# Allocated quantity cannot exceed available stock quantity
if self.quantity > self.stock_item.quantity:
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format(

View File

@ -86,7 +86,7 @@
}
);
});
$("#btn-order-parts").click(function() {
launchModalForm("/order/purchase-order/order-parts/", {
data: {
@ -94,7 +94,7 @@
},
});
});
{% endif %}
{% endblock %}

View File

@ -230,5 +230,5 @@ src="{% static 'img/blank_image.png' %}"
}
);
});
{% endblock %}

View File

@ -17,9 +17,9 @@
<div class='col-sm-6'>
</div>
<hr>
<div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group'>
@ -66,7 +66,7 @@
<script type='text/javascript'>
function loadOrderEvents(calendar) {
var start = startDate(calendar);
var end = endDate(calendar);
@ -85,7 +85,7 @@
var prefix = '{% settings_value "BUILDORDER_REFERENCE_PREFIX" %}';
for (var idx = 0; idx < response.length; idx++) {
var order = response[idx];
var date = order.creation_date;
@ -155,7 +155,7 @@ $('#view-calendar').click(function() {
$(".fixed-table-pagination").hide();
$(".columns-right").hide();
$(".search").hide();
$("#build-order-calendar").show();
$("#view-list").show();
@ -166,7 +166,7 @@ $("#view-list").click(function() {
// Hide the calendar view, show the list view
$("#build-order-calendar").hide();
$("#view-list").hide();
$(".fixed-table-pagination").show();
$(".columns-right").show();
$(".search").show();

View File

@ -17,7 +17,7 @@
</li>
{% if build.active %}
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "Allocate Stock" %}'>
<a href='{% url "build-allocate" build.id %}'>
<span class='fas fa-tools'></span>

View File

@ -20,11 +20,11 @@
<hr>
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
</form>
{{ form.media }}

View File

@ -30,7 +30,7 @@ class BuildAPITest(InvenTreeAPITestCase):
'build.change',
'build.add'
]
def setUp(self):
super().setUp()
@ -54,7 +54,7 @@ class BuildListTest(BuildAPITest):
builds = self.get(self.url, data={'active': True})
self.assertEqual(len(builds.data), 1)
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
self.assertEqual(len(builds.data), 4)

View File

@ -114,7 +114,7 @@ class BuildTest(TestCase):
# Perform some basic tests before we start the ball rolling
self.assertEqual(StockItem.objects.count(), 6)
# Build is PENDING
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
@ -142,7 +142,7 @@ class BuildTest(TestCase):
# Create a BuiltItem which points to an invalid StockItem
b = BuildItem(stock_item=stock, build=self.build, quantity=10)
with self.assertRaises(ValidationError):
b.save()
@ -339,7 +339,7 @@ class BuildTest(TestCase):
self.assertTrue(self.build.can_complete)
self.build.complete_build(None)
self.assertEqual(self.build.status, status.BuildStatus.COMPLETE)
# the original BuildItem objects should have been deleted!
@ -351,12 +351,12 @@ class BuildTest(TestCase):
# This stock item has been depleted!
with self.assertRaises(StockItem.DoesNotExist):
StockItem.objects.get(pk=self.stock_1_1.pk)
# This stock item has *not* been depleted
x = StockItem.objects.get(pk=self.stock_2_1.pk)
self.assertEqual(x.quantity, 4970)
# And 10 new stock items created for the build output
outputs = StockItem.objects.filter(build=self.build)

View File

@ -251,7 +251,7 @@ class TestBuildViews(TestCase):
content = str(response.content)
self.assertIn(build.title, content)
def test_build_create(self):
""" Test the build creation view (ajax form) """
@ -260,7 +260,7 @@ class TestBuildViews(TestCase):
# Create build without specifying part
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Create build with valid part
response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
@ -281,7 +281,7 @@ class TestBuildViews(TestCase):
# Get the page in editing mode
response = self.client.get(url, {'edit': 1})
self.assertEqual(response.status_code, 200)
def test_build_item_create(self):
""" Test the BuildItem creation view (ajax form) """
@ -305,7 +305,7 @@ class TestBuildViews(TestCase):
def test_build_item_edit(self):
""" Test the BuildItem edit view (ajax form) """
# TODO
# url = reverse('build-item-edit')
pass
@ -323,7 +323,7 @@ class TestBuildViews(TestCase):
# Test without confirmation
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
@ -353,7 +353,7 @@ class TestBuildViews(TestCase):
# Test with confirmation, invalid location
response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
@ -365,7 +365,7 @@ class TestBuildViews(TestCase):
# Test without confirmation
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
@ -393,7 +393,7 @@ class TestBuildViews(TestCase):
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
# Test with confirmation
response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)

View File

@ -159,7 +159,7 @@ class BuildOutputCreate(AjaxUpdateView):
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:
@ -318,7 +318,7 @@ class BuildUnallocate(AjaxUpdateView):
form_class = forms.UnallocateBuildForm
ajax_form_title = _("Unallocate Stock")
ajax_template_name = "build/unallocate.html"
def get_initial(self):
initials = super().get_initial()
@ -341,7 +341,7 @@ class BuildUnallocate(AjaxUpdateView):
build = self.get_object()
form = self.get_form()
confirm = request.POST.get('confirm', False)
output_id = request.POST.get('output_id', None)
@ -382,7 +382,7 @@ class BuildUnallocate(AjaxUpdateView):
# Unallocate "untracked" parts
else:
build.unallocateUntracked(part=part)
data = {
'form_valid': valid,
}
@ -401,7 +401,7 @@ class BuildComplete(AjaxUpdateView):
model = Build
form_class = forms.CompleteBuildForm
ajax_form_title = _('Complete Build Order')
ajax_template_name = 'build/complete.html'
@ -437,9 +437,9 @@ class BuildOutputComplete(AjaxUpdateView):
context_object_name = "build"
ajax_form_title = _("Complete Build Output")
ajax_template_name = "build/complete_output.html"
def get_form(self):
build = self.get_object()
form = super().get_form()
@ -500,7 +500,7 @@ class BuildOutputComplete(AjaxUpdateView):
- If the part being built has a default location, pre-select that location
"""
initials = super().get_initial()
build = self.get_object()
@ -585,7 +585,7 @@ class BuildOutputComplete(AjaxUpdateView):
location=location,
status=stock_status,
)
def get_data(self):
""" Provide feedback data back to the form """
return {
@ -600,7 +600,7 @@ class BuildNotes(InvenTreeRoleMixin, UpdateView):
context_object_name = 'build'
template_name = 'build/notes.html'
model = Build
# Override the default permission role for this View
role_required = 'build.view'
@ -612,7 +612,7 @@ class BuildNotes(InvenTreeRoleMixin, UpdateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
return ctx
@ -746,7 +746,7 @@ class BuildCreate(AjaxCreateView):
class BuildUpdate(AjaxUpdateView):
""" View for editing a Build object """
model = Build
form_class = forms.EditBuildForm
context_object_name = 'build'
@ -804,7 +804,7 @@ class BuildItemDelete(AjaxDeleteView):
ajax_template_name = 'build/delete_build_item.html'
ajax_form_title = _('Unallocate Stock')
context_object_name = 'item'
def get_data(self):
return {
'danger': _('Removed parts from build allocation')
@ -826,7 +826,7 @@ class BuildItemCreate(AjaxCreateView):
# The "part" which is being allocated to the output
part = None
available_stock = None
def get_context_data(self):
@ -906,7 +906,7 @@ class BuildItemCreate(AjaxCreateView):
if part_id:
try:
self.part = Part.objects.get(pk=part_id)
except (ValueError, Part.DoesNotExist):
pass
@ -958,7 +958,7 @@ class BuildItemCreate(AjaxCreateView):
# Reference to a StockItem object
item = None
# Reference to a Build object
build = None
@ -999,7 +999,7 @@ class BuildItemCreate(AjaxCreateView):
quantity = float(quantity)
elif required_quantity is not None:
quantity = required_quantity
item_id = self.get_param('item')
# If the request specifies a particular StockItem
@ -1035,7 +1035,7 @@ class BuildItemEdit(AjaxUpdateView):
ajax_template_name = 'build/edit_build_item.html'
form_class = forms.EditBuildItemForm
ajax_form_title = _('Edit Stock Allocation')
def get_data(self):
return {
'info': _('Updated Build Item'),
@ -1068,7 +1068,7 @@ class BuildAttachmentCreate(AjaxCreateView):
model = BuildOrderAttachment
form_class = forms.EditBuildAttachmentForm
ajax_form_title = _('Add Build Order Attachment')
def save(self, form, **kwargs):
"""
Add information on the user that uploaded the attachment
@ -1105,7 +1105,7 @@ class BuildAttachmentCreate(AjaxCreateView):
form = super().get_form()
form.fields['build'].widget = HiddenInput()
return form

View File

@ -9,7 +9,7 @@ from .models import InvenTreeSetting
class SettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value')

View File

@ -42,7 +42,7 @@ class InvenTreeSetting(models.Model):
The key of each item is the name of the value as it appears in the database.
Each global setting has the following parameters:
- name: Translatable string name of the setting (required)
- description: Translatable string description of the setting (required)
- default: Default value (optional)
@ -414,7 +414,7 @@ class InvenTreeSetting(models.Model):
# Evaluate the function (we expect it will return a list of tuples...)
return choices()
"""
return choices
@classmethod
@ -524,7 +524,7 @@ class InvenTreeSetting(models.Model):
# Enforce standard boolean representation
if setting.is_bool():
value = InvenTree.helpers.str2bool(value)
setting.value = str(value)
setting.save()
@ -666,7 +666,7 @@ class InvenTreeSetting(models.Model):
if validator == int:
return True
if type(validator) in [list, tuple]:
for v in validator:
if v == int:
@ -677,7 +677,7 @@ class InvenTreeSetting(models.Model):
def as_int(self):
"""
Return the value of this setting converted to a boolean value.
If an error occurs, return the default value
"""
@ -687,7 +687,7 @@ class InvenTreeSetting(models.Model):
value = self.default_value()
return value
class PriceBreak(models.Model):
"""

View File

@ -19,7 +19,7 @@ def currency_code_default():
if code not in CURRENCIES:
code = 'USD'
return code

View File

@ -117,7 +117,7 @@ class SettingsViewTest(TestCase):
"""
Test for binary value
"""
setting = InvenTreeSetting.get_setting_object('PART_COMPONENT')
self.assertTrue(setting.as_bool())

View File

@ -19,7 +19,7 @@ class SettingsTest(TestCase):
def setUp(self):
user = get_user_model()
self.user = user.objects.create_user('username', 'user@email.com', 'password')
self.user.is_staff = True
self.user.save()

View File

@ -55,7 +55,7 @@ class SettingEdit(AjaxUpdateView):
"""
form = super().get_form()
setting = self.get_object()
choices = setting.choices()

View File

@ -41,7 +41,7 @@ class CompanyList(generics.ListCreateAPIView):
queryset = CompanySerializer.annotate_queryset(queryset)
return queryset
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
@ -116,7 +116,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
@ -167,7 +167,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
'part__name',
'part__description',
]
class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of ManufacturerPart object
@ -255,7 +255,7 @@ class SupplierPartList(generics.ListCreateAPIView):
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
except AttributeError:
pass
try:
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', None))
except AttributeError:
@ -270,7 +270,7 @@ class SupplierPartList(generics.ListCreateAPIView):
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)

View File

@ -24,7 +24,7 @@
name: A customer
description: A company that we sell things to!
is_customer: True
- model: company.company
pk: 5
fields:

View File

@ -158,7 +158,7 @@ class EditSupplierPartForm(HelperForm):
empty_choice = [('', '----------')]
manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)]
return empty_choice + manufacturers
def __init__(self, *args, **kwargs):

View File

@ -149,7 +149,7 @@ class Company(models.Model):
def currency_code(self):
"""
Return the currency code associated with this company.
- If the currency code is invalid, use the default currency
- If the currency code is not specified, use the default currency
"""
@ -184,7 +184,7 @@ class Company(models.Model):
return getMediaUrl(self.image.thumbnail.url)
else:
return getBlankThumbnail()
@property
def manufactured_part_count(self):
""" The number of parts manufactured by this company """
@ -299,7 +299,7 @@ class ManufacturerPart(models.Model):
class Meta:
unique_together = ('part', 'manufacturer', 'MPN')
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='manufacturer_parts',
verbose_name=_('Base Part'),
@ -308,7 +308,7 @@ class ManufacturerPart(models.Model):
},
help_text=_('Select part'),
)
manufacturer = models.ForeignKey(
Company,
on_delete=models.CASCADE,
@ -356,7 +356,7 @@ class ManufacturerPart(models.Model):
if not manufacturer_part:
manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=mpn, description=description, link=link)
manufacturer_part.save()
return manufacturer_part
def __str__(self):
@ -411,7 +411,7 @@ class SupplierPart(models.Model):
MPN = kwargs.pop('MPN')
else:
MPN = None
if manufacturer or MPN:
if not self.manufacturer_part:
# Create ManufacturerPart
@ -426,7 +426,7 @@ class SupplierPart(models.Model):
manufacturer_part_id = self.manufacturer_part.id
except AttributeError:
manufacturer_part_id = None
if manufacturer_part_id:
try:
(manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part,
@ -501,7 +501,7 @@ class SupplierPart(models.Model):
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)'))
packaging = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('Packaging'), help_text=_('Part packaging'))
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name=_('multiple'), help_text=_('Order multiple'))
# TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
@ -603,7 +603,7 @@ class SupplierPart(models.Model):
if self.manufacturer_string:
s = s + ' | ' + self.manufacturer_string
return s

View File

@ -51,7 +51,7 @@ class CompanySerializer(InvenTreeModelSerializer):
return queryset
url = serializers.CharField(source='get_absolute_url', read_only=True)
image = serializers.CharField(source='get_thumbnail_url', read_only=True)
parts_supplied = serializers.IntegerField(read_only=True)
@ -157,9 +157,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
self.fields.pop('pretty_name')
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True)
MPN = serializers.StringRelatedField(source='manufacturer_part.MPN')
manufacturer_part = ManufacturerPartSerializer(read_only=True)

View File

@ -75,5 +75,5 @@
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% endblock %}

View File

@ -6,7 +6,7 @@
{% trans "Are you sure you want to delete the following Manufacturer Parts?" %}
</div>
{% for part in parts %}
{% endfor %}
{% endblock %}
@ -17,7 +17,7 @@
<table class='table table-striped table-condensed'>
<tr>
<input type='hidden' name='manufacturer-part-{{ part.id}}' value='manufacturer-part-{{ part.id }}'/>
<td>
{% include "hover_image.html" with image=part.part.image %}
{{ part.part.full_name }}

View File

@ -33,6 +33,6 @@
{% block js_ready %}
{{ block.super }}
{% endblock %}

View File

@ -53,7 +53,7 @@ $('#supplier-create').click(function () {
});
$("#supplier-part-delete").click(function() {
var selections = $("#supplier-table").bootstrapTable("getSelections");
var parts = [];

View File

@ -24,7 +24,7 @@
</a>
</li>
{% endif %}
{% if company.is_supplier or company.is_manufacturer %}
<li class='list-group-item {% if tab == "supplier_parts" %}active{% endif %}' title='{% trans "Supplied Parts" %}'>
<a href='{% url "company-detail-supplier-parts" company.id %}'>

View File

@ -18,11 +18,11 @@
{% if editing %}
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
</form>
{{ form.media }}
@ -43,5 +43,5 @@ $("#edit-notes").click(function() {
location.href = "{% url 'company-notes' company.id %}?edit=1";
});
{% endif %}
{% endblock %}

View File

@ -12,7 +12,7 @@
{% for part in parts %}
<tr>
<input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
<td>
{% include "hover_image.html" with image=part.part.image %}
{{ part.part.full_name }}

View File

@ -43,6 +43,6 @@
{% block js_ready %}
{{ block.super }}
{% endblock %}

View File

@ -19,7 +19,7 @@
</div>
</div>
{% endif %}
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
</table>

View File

@ -90,7 +90,7 @@ $('#price-break-table').inventreeTable({
html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
html += `</div>`;
return html;
}
},

View File

@ -48,7 +48,7 @@ class TestManufacturerField(MigratorTestCase):
- Company object (supplier)
- SupplierPart object
"""
Part = self.old_state.apps.get_model('part', 'part')
Company = self.old_state.apps.get_model('company', 'company')
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
@ -123,7 +123,7 @@ class TestManufacturerPart(MigratorTestCase):
- Company object (supplier)
- SupplierPart object
"""
Part = self.old_state.apps.get_model('part', 'part')
Company = self.old_state.apps.get_model('company', 'company')
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
@ -220,7 +220,7 @@ class TestManufacturerPart(MigratorTestCase):
# Check on the SupplierPart objects
SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
supplier_parts = SupplierPart.objects.all()
self.assertEqual(supplier_parts.count(), 6)
@ -229,10 +229,10 @@ class TestManufacturerPart(MigratorTestCase):
# Check on the ManufacturerPart objects
ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart')
manufacturer_parts = ManufacturerPart.objects.all()
self.assertEqual(manufacturer_parts.count(), 4)
manufacturer_part = manufacturer_parts.first()
self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456')
@ -293,7 +293,7 @@ class TestCurrencyMigration(MigratorTestCase):
self.assertIsNone(pb.price)
def test_currency_migration(self):
PB = self.new_state.apps.get_model('company', 'supplierpricebreak')
for pb in PB.objects.all():

View File

@ -30,7 +30,7 @@ class CompanyViewTestBase(TestCase):
# Create a user
user = get_user_model()
self.user = user.objects.create_user(
username='username',
email='user@email.com',
@ -83,7 +83,7 @@ class SupplierPartViewTests(CompanyViewTestBase):
def test_supplier_part_create(self):
"""
Test the SupplierPartCreate view.
This view allows some additional functionality,
specifically it allows the user to create a single-quantity price break
automatically, when saving the new SupplierPart model.
@ -171,7 +171,7 @@ class SupplierPartViewTests(CompanyViewTestBase):
'confirm_delete': True
},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertEqual(n - 2, SupplierPart.objects.count())
@ -213,7 +213,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
"""
Test the ManufacturerPartCreate view.
"""
url = reverse('manufacturer-part-create')
# First check that we can GET the form
@ -252,7 +252,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
"""
Test that the SupplierPartCreate view creates Manufacturer Part.
"""
url = reverse('supplier-part-create')
# First check that we can GET the form
@ -297,7 +297,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
'confirm_delete': True
},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Check that the ManufacturerPart was deleted

View File

@ -71,7 +71,7 @@ class CompanySimpleTest(TestCase):
acme = Company.objects.get(pk=1)
appel = Company.objects.get(pk=2)
zerg = Company.objects.get(pk=3)
self.assertTrue(acme.has_parts)
self.assertEqual(acme.supplied_part_count, 4)
@ -82,7 +82,7 @@ class CompanySimpleTest(TestCase):
self.assertEqual(zerg.supplied_part_count, 2)
def test_price_breaks(self):
self.assertTrue(self.acme0001.has_price_breaks)
self.assertTrue(self.acme0002.has_price_breaks)
self.assertTrue(self.zergm312.has_price_breaks)
@ -121,7 +121,7 @@ class CompanySimpleTest(TestCase):
pmin, pmax = m2x4.get_price_range(5)
self.assertEqual(pmin, 35)
self.assertEqual(pmax, 37.5)
m3x12 = Part.objects.get(name='M3x12 SHCS')
self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4'))
@ -187,14 +187,14 @@ class ManufacturerPartSimpleTest(TestCase):
# Create a manufacturer part
self.part = Part.objects.get(pk=1)
manufacturer = Company.objects.get(pk=1)
self.mp = ManufacturerPart.create(
part=self.part,
manufacturer=manufacturer,
mpn='PART_NUMBER',
description='THIS IS A MANUFACTURER PART',
)
# Create a supplier part
supplier = Company.objects.get(pk=5)
supplier_part = SupplierPart.objects.create(

View File

@ -55,7 +55,7 @@ price_break_urls = [
manufacturer_part_detail_urls = [
url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'),
url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'),
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'),

View File

@ -96,7 +96,7 @@ class CompanyIndex(InvenTreeRoleMixin, ListView):
if self.request.path == item:
context = lookup[item]
break
if context is None:
context = default
@ -279,7 +279,7 @@ class CompanyCreate(AjaxCreateView):
if url == reverse('supplier-create'):
return _("Create new Supplier")
if url == reverse('manufacturer-create'):
return _('Create new Manufacturer')
@ -298,7 +298,7 @@ class CompanyCreate(AjaxCreateView):
initials['is_supplier'] = True
initials['is_customer'] = False
initials['is_manufacturer'] = False
elif url == reverse('manufacturer-create'):
initials['is_manufacturer'] = True
initials['is_supplier'] = True
@ -319,7 +319,7 @@ class CompanyCreate(AjaxCreateView):
class CompanyDelete(AjaxDeleteView):
""" View for deleting a Company object """
model = Company
success_url = '/company/'
ajax_template_name = 'company/delete.html'
@ -415,7 +415,7 @@ class ManufacturerPartCreate(AjaxCreateView):
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
except (ValueError, Company.DoesNotExist):
pass
if part_id:
try:
initials['part'] = Part.objects.get(pk=part_id)
@ -427,7 +427,7 @@ class ManufacturerPartCreate(AjaxCreateView):
class ManufacturerPartDelete(AjaxDeleteView):
""" Delete view for removing a ManufacturerPart.
ManufacturerParts can be deleted using a variety of 'selectors'.
- ?part=<pk> -> Delete a single ManufacturerPart object
@ -561,7 +561,7 @@ class SupplierPartEdit(AjaxUpdateView):
initials = super(SupplierPartEdit, self).get_initial().copy()
supplier_part = self.get_object()
if supplier_part.manufacturer_part:
initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
initials['MPN'] = supplier_part.manufacturer_part.MPN
@ -686,7 +686,7 @@ class SupplierPartCreate(AjaxCreateView):
initials['MPN'] = manufacturer_part_obj.MPN
except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
pass
if part_id:
try:
initials['part'] = Part.objects.get(pk=part_id)
@ -703,13 +703,13 @@ class SupplierPartCreate(AjaxCreateView):
if currency_code:
initials['single_pricing'] = ('', currency)
return initials
class SupplierPartDelete(AjaxDeleteView):
""" Delete view for removing a SupplierPart.
SupplierParts can be deleted using a variety of 'selectors'.
- ?part=<pk> -> Delete a single SupplierPart object
@ -840,7 +840,7 @@ class PriceBreakCreate(AjaxCreateView):
# Extract the currency object associated with the code
currency = CURRENCIES.get(currency_code, None)
if currency:
initials['price'] = [1.0, currency]

View File

@ -11,7 +11,7 @@ database:
# or specify database options using environment variables
# Refer to the django documentation for full list of options
# --- Available options: ---
# ENGINE: Database engine. Selection from:
# - sqlite3
@ -114,7 +114,7 @@ allowed_hosts:
cors:
# CORS_ORIGIN_ALLOW_ALL - If True, the whitelist will not be used and all origins will be accepted.
allow_all: True
# CORS_ORIGIN_WHITELIST - A list of origins that are authorized to make cross-site HTTP requests. Defaults to []
# whitelist:
# - https://example.com

View File

@ -159,7 +159,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
"""
Filter the StockItem label queryset.
"""
queryset = super().filter_queryset(queryset)
# List of StockItem objects to match against
@ -178,7 +178,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
# Keep track of which labels match every specified stockitem
valid_label_ids = set()
for label in queryset.all():
matches = True
@ -293,7 +293,7 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin):
"""
queryset = super().filter_queryset(queryset)
# List of StockLocation objects to match against
locations = self.get_locations()

View File

@ -139,7 +139,7 @@ class LabelConfig(AppConfig):
except:
# Database might not yet be ready
return
src_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'templates',

View File

@ -44,7 +44,7 @@ def rename_label(instance, filename):
def validate_stock_item_filters(filters):
filters = validateFilterString(filters, model=stock.models.StockItem)
return filters
@ -82,7 +82,7 @@ class LabelTemplate(models.Model):
# Each class of label files will be stored in a separate subdirectory
SUBDIR = "label"
# Object we will be printing against (will be filled out later)
object_to_print = None

View File

@ -40,7 +40,7 @@ class TestReportTests(InvenTreeAPITestCase):
return response.data
def test_list(self):
response = self.do_list()
# TODO - Add some report templates to the fixtures

View File

@ -64,7 +64,7 @@ class CancelSalesOrderForm(HelperForm):
fields = [
'confirm',
]
class ShipSalesOrderForm(HelperForm):

View File

@ -309,7 +309,7 @@ class PurchaseOrder(Order):
"""
A PurchaseOrder can only be cancelled under the following circumstances:
"""
return self.status in [
PurchaseOrderStatus.PLACED,
PurchaseOrderStatus.PENDING
@ -378,7 +378,7 @@ class PurchaseOrder(Order):
# Has this order been completed?
if len(self.pending_line_items()) == 0:
self.received_by = user
self.complete_order() # This will save the model
@ -419,7 +419,7 @@ class SalesOrder(Order):
except (ValueError, TypeError):
# Date processing error, return queryset unchanged
return queryset
# Construct a queryset for "completed" orders within the range
completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date)
@ -495,7 +495,7 @@ class SalesOrder(Order):
for line in self.lines.all():
if not line.is_fully_allocated():
return False
return True
def is_over_allocated(self):
@ -590,11 +590,11 @@ class SalesOrderAttachment(InvenTreeAttachment):
class OrderLineItem(models.Model):
""" Abstract model for an order line item
Attributes:
quantity: Number of items
note: Annotation for the item
"""
class Meta:
@ -603,13 +603,13 @@ class OrderLineItem(models.Model):
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'), help_text=_('Item quantity'))
reference = models.CharField(max_length=100, blank=True, verbose_name=_('Reference'), help_text=_('Line item reference'))
notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes'))
class PurchaseOrderLineItem(OrderLineItem):
""" Model for a purchase order line item.
Attributes:
order: Reference to a PurchaseOrder object
@ -637,7 +637,7 @@ class PurchaseOrderLineItem(OrderLineItem):
def get_base_part(self):
""" Return the base-part for the line item """
return self.part.part
# TODO - Function callback for when the SupplierPart is deleted?
part = models.ForeignKey(

View File

@ -61,7 +61,7 @@ class POSerializer(InvenTreeModelSerializer):
return queryset
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
line_items = serializers.IntegerField(read_only=True)
status_text = serializers.CharField(source='get_status_display', read_only=True)
@ -70,7 +70,7 @@ class POSerializer(InvenTreeModelSerializer):
class Meta:
model = PurchaseOrder
fields = [
'pk',
'issue_date',
@ -89,7 +89,7 @@ class POSerializer(InvenTreeModelSerializer):
'target_date',
'notes',
]
read_only_fields = [
'reference',
'status'
@ -110,10 +110,10 @@ class POLineItemSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField()
received = serializers.FloatField()
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True)
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
class Meta:
@ -144,7 +144,7 @@ class POAttachmentSerializer(InvenTreeModelSerializer):
class Meta:
model = PurchaseOrderAttachment
fields = [
'pk',
'order',
@ -270,7 +270,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
if allocations is not True:
self.fields.pop('allocations')
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
allocations = SalesOrderAllocationSerializer(many=True, read_only=True)
@ -310,7 +310,7 @@ class SOAttachmentSerializer(InvenTreeModelSerializer):
class Meta:
model = SalesOrderAttachment
fields = [
'pk',
'order',

View File

@ -44,7 +44,7 @@ $("#new-attachment").click(function() {
$("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this);
var url = `/order/purchase-order/attachment/${button.attr('pk')}/edit/`;
launchModalForm(url, {

View File

@ -1,5 +1,6 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% 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 %}

View File

@ -193,11 +193,11 @@ $("#po-table").inventreeTable({
});
},
sorter: function(valA, valB, rowA, rowB) {
if (rowA.received == 0 && rowB.received == 0) {
return (rowA.quantity > rowB.quantity) ? 1 : -1;
}
var progressA = parseFloat(rowA.received) / rowA.quantity;
var progressB = parseFloat(rowB.received) / rowB.quantity;

View File

@ -83,7 +83,7 @@
var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`;
var color = '#4c68f5';
if (order.complete_date) {
color = '#25c235';
} else if (order.overdue) {
@ -143,7 +143,7 @@ $('#view-calendar').click(function() {
$(".columns-right").hide();
$(".search").hide();
$('#filter-list-salesorder').hide();
$("#purchase-order-calendar").show();
$("#view-list").show();
@ -154,7 +154,7 @@ $("#view-list").click(function() {
// Hide the calendar view, show the list view
$("#purchase-order-calendar").hide();
$("#view-list").hide();
$(".fixed-table-pagination").show();
$(".columns-right").show();
$(".search").show();

View File

@ -51,13 +51,13 @@ $("#new-so-line").click(function() {
{% if order.status == SalesOrderStatus.PENDING %}
function showAllocationSubTable(index, row, element) {
// Construct a table showing stock items which have been allocated against this line item
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`;
element.html(html);
var lineItem = row;
var table = $(`#allocation-table-${row.pk}`);
table.bootstrapTable({
@ -70,7 +70,7 @@ function showAllocationSubTable(index, row, element) {
title: '{% trans "Quantity" %}',
formatter: function(value, row, index, field) {
var text = '';
if (row.serial != null && row.quantity == 1) {
text = `{% trans "Serial Number" %}: ${row.serial}`;
} else {
@ -91,10 +91,10 @@ function showAllocationSubTable(index, row, element) {
field: 'buttons',
title: '{% trans "Actions" %}',
formatter: function(value, row, index, field) {
var html = "<div class='btn-group float-right' role='group'>";
var pk = row.pk;
{% if order.status == SalesOrderStatus.PENDING %}
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
@ -256,11 +256,11 @@ $("#so-lines-table").inventreeTable({
var A = rowA.fulfilled;
var B = rowB.fulfilled;
{% endif %}
if (A == 0 && B == 0) {
return (rowA.quantity > rowB.quantity) ? 1 : -1;
}
var progressA = parseFloat(A) / rowA.quantity;
var progressB = parseFloat(B) / rowB.quantity;
@ -279,7 +279,7 @@ $("#so-lines-table").inventreeTable({
var html = `<div class='btn-group float-right' role='group'>`;
var pk = row.pk;
if (row.part) {
var part = row.part_detail;
@ -292,14 +292,14 @@ $("#so-lines-table").inventreeTable({
if (part.purchaseable) {
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
}
if (part.assembly) {
html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}');
}
html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}');
}
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}');

View File

@ -152,7 +152,7 @@ $("#view-list").click(function() {
// Hide the calendar view, show the list view
$("#sales-order-calendar").hide();
$("#view-list").hide();
$(".fixed-table-pagination").show();
$(".columns-right").show();
$(".search").show();

View File

@ -94,7 +94,7 @@ class PurchaseOrderTest(OrderTest):
url = '/api/order/po/1/'
response = self.get(url)
self.assertEqual(response.status_code, 200)
data = response.data
@ -109,7 +109,7 @@ class PurchaseOrderTest(OrderTest):
response = self.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
class SalesOrderTest(OrderTest):
"""

View File

@ -73,7 +73,7 @@ class SalesOrderTest(TestCase):
def test_add_duplicate_line_item(self):
# Adding a duplicate line item to a SalesOrder is accepted
for ii in range(1, 5):
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii)
@ -107,7 +107,7 @@ class SalesOrderTest(TestCase):
self.assertTrue(self.order.is_fully_allocated())
self.assertTrue(self.line.is_fully_allocated())
self.assertEqual(self.line.allocated_quantity(), 50)
def test_order_cancel(self):
# Allocate line items then cancel the order
@ -154,7 +154,7 @@ class SalesOrderTest(TestCase):
for item in outputs.all():
self.assertEqual(item.quantity, 25)
self.assertEqual(sa.sales_order, None)
self.assertEqual(sb.sales_order, None)
@ -162,7 +162,7 @@ class SalesOrderTest(TestCase):
self.assertEqual(SalesOrderAllocation.objects.count(), 0)
self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED)
self.assertTrue(self.order.is_fully_allocated())
self.assertTrue(self.line.is_fully_allocated())
self.assertEqual(self.line.fulfilled_quantity(), 50)

View File

@ -17,7 +17,7 @@ import json
class OrderViewTestCase(TestCase):
fixtures = [
'category',
'part',
@ -193,7 +193,7 @@ class POTests(OrderViewTestCase):
# Test without confirmation
response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
@ -221,7 +221,7 @@ class POTests(OrderViewTestCase):
# GET the form (pass the correct info)
response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
post_data = {
'part': 100,
'quantity': 45,
@ -303,7 +303,7 @@ class TestPOReceive(OrderViewTestCase):
self.client.get(self.url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
def test_receive_lines(self):
post_data = {
}
@ -330,7 +330,7 @@ class TestPOReceive(OrderViewTestCase):
# Receive negative number
post_data['line-1'] = -100
self.post(post_data, validate=False)
# Receive 75 items

View File

@ -36,7 +36,7 @@ class OrderTest(TestCase):
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
self.assertEqual(str(order), 'PO0001 - ACME')
line = PurchaseOrderLineItem.objects.get(pk=1)
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
@ -113,7 +113,7 @@ class OrderTest(TestCase):
# Try to order a supplier part from the wrong supplier
sku = SupplierPart.objects.get(SKU='ZERG-WIDGET')
with self.assertRaises(django_exceptions.ValidationError):
order.add_line_item(sku, 99)
@ -153,7 +153,7 @@ class OrderTest(TestCase):
with self.assertRaises(django_exceptions.ValidationError):
order.receive_line_item(line, loc, 'not a number', user=None)
# Receive the rest of the items
order.receive_line_item(line, loc, 50, user=None)

View File

@ -157,7 +157,7 @@ class SalesOrderAttachmentCreate(AjaxCreateView):
"""
Save the user that uploaded the attachment
"""
attachment = form.save(commit=False)
attachment.user = self.request.user
attachment.save()
@ -335,7 +335,7 @@ class PurchaseOrderCreate(AjaxCreateView):
order = form.save(commit=False)
order.created_by = self.request.user
return super().save(form)
@ -370,7 +370,7 @@ class SalesOrderCreate(AjaxCreateView):
order = form.save(commit=False)
order.created_by = self.request.user
return super().save(form)
@ -419,7 +419,7 @@ class PurchaseOrderCancel(AjaxUpdateView):
form_class = order_forms.CancelPurchaseOrderForm
def validate(self, order, form, **kwargs):
confirm = str2bool(form.cleaned_data.get('confirm', False))
if not confirm:
@ -541,11 +541,11 @@ class SalesOrderShip(AjaxUpdateView):
order = self.get_object()
self.object = order
form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False))
valid = False
if not confirm:
@ -1025,7 +1025,7 @@ class OrderParts(AjaxView):
for supplier in self.suppliers:
supplier.order_items = []
suppliers[supplier.name] = supplier
for part in self.parts:
@ -1046,9 +1046,9 @@ class OrderParts(AjaxView):
supplier.selected_purchase_order = orders.first().id
else:
supplier.selected_purchase_order = None
suppliers[supplier.name] = supplier
suppliers[supplier.name].order_items.append(part)
self.suppliers = [suppliers[key] for key in suppliers.keys()]
@ -1066,7 +1066,7 @@ class OrderParts(AjaxView):
if 'stock[]' in self.request.GET:
stock_id_list = self.request.GET.getlist('stock[]')
""" Get a list of all the parts associated with the stock items.
- Base part must be purchaseable.
- Return a set of corresponding Part IDs
@ -1109,7 +1109,7 @@ class OrderParts(AjaxView):
parts = build.required_parts
for part in parts:
# If ordering from a Build page, ignore parts that we have enough of
if part.quantity_to_order <= 0:
continue
@ -1165,19 +1165,19 @@ class OrderParts(AjaxView):
# Extract part information from the form
for item in self.request.POST:
if item.startswith('part-supplier-'):
pk = item.replace('part-supplier-', '')
# Check that the part actually exists
try:
part = Part.objects.get(id=pk)
except (Part.DoesNotExist, ValueError):
continue
supplier_part_id = self.request.POST[item]
quantity = self.request.POST.get('part-quantity-' + str(pk), 0)
# Ensure a valid supplier has been passed
@ -1591,7 +1591,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin):
self.form.fields['line'].widget = HiddenInput()
else:
self.form.add_error('line', _('Select line item'))
if self.part:
self.form.fields['part'].widget = HiddenInput()
else:
@ -1626,7 +1626,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin):
continue
# Now we have a valid stock item - but can it be added to the sales order?
# If not in stock, cannot be added to the order
if not stock_item.in_stock:
self.form.add_error(
@ -1694,7 +1694,7 @@ class SalesOrderAllocationCreate(AjaxCreateView):
model = SalesOrderAllocation
form_class = order_forms.CreateSalesOrderAllocationForm
ajax_form_title = _('Allocate Stock to Order')
def get_initial(self):
initials = super().get_initial().copy()
@ -1709,10 +1709,10 @@ class SalesOrderAllocationCreate(AjaxCreateView):
items = StockItem.objects.filter(part=line.part)
quantity = line.quantity - line.allocated_quantity()
if quantity < 0:
quantity = 0
if items.count() == 1:
item = items.first()
initials['item'] = item
@ -1728,7 +1728,7 @@ class SalesOrderAllocationCreate(AjaxCreateView):
return initials
def get_form(self):
form = super().get_form()
line_id = form['line'].value()
@ -1756,10 +1756,10 @@ class SalesOrderAllocationCreate(AjaxCreateView):
# Hide the 'line' field
form.fields['line'].widget = HiddenInput()
except (ValueError, SalesOrderLineItem.DoesNotExist):
pass
return form
@ -1768,7 +1768,7 @@ class SalesOrderAllocationEdit(AjaxUpdateView):
model = SalesOrderAllocation
form_class = order_forms.EditSalesOrderAllocationForm
ajax_form_title = _('Edit Allocation Quantity')
def get_form(self):
form = super().get_form()

View File

@ -25,13 +25,13 @@ class PartResource(ModelResource):
# ForeignKey fields
category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory))
default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation))
default_supplier = Field(attribute='default_supplier', widget=widgets.ForeignKeyWidget(SupplierPart))
category_name = Field(attribute='category__name', readonly=True)
variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part))
suppliers = Field(attribute='supplier_count', readonly=True)
@ -73,7 +73,7 @@ class PartResource(ModelResource):
class PartAdmin(ImportExportModelAdmin):
resource_class = PartResource
list_display = ('full_name', 'description', 'total_stock', 'category')

View File

@ -41,7 +41,7 @@ class PartCategoryTree(TreeSerializer):
model = PartCategory
queryset = PartCategory.objects.all()
@property
def root_url(self):
return reverse('part-index')
@ -79,7 +79,7 @@ class CategoryList(generics.ListCreateAPIView):
pass
# Look for top-level categories
elif isNull(cat_id):
if not cascade:
queryset = queryset.filter(parent=None)
@ -166,9 +166,9 @@ class CategoryParameters(generics.ListAPIView):
parent_categories = category.get_ancestors()
for parent in parent_categories:
category_list.append(parent.pk)
queryset = queryset.filter(category__in=category_list)
return queryset
@ -264,7 +264,7 @@ class PartThumbs(generics.ListAPIView):
# Get all Parts which have an associated image
queryset = queryset.exclude(image='')
return queryset
def list(self, request, *args, **kwargs):
@ -301,7 +301,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Part.objects.all()
serializer_class = part_serializers.PartSerializer
starred_parts = None
def get_queryset(self, *args, **kwargs):
@ -482,7 +482,7 @@ class PartList(generics.ListCreateAPIView):
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
@ -576,7 +576,7 @@ class PartList(generics.ListCreateAPIView):
if cat_id is None:
# No category filtering if category is not specified
pass
else:
# Category has been specified!
if isNull(cat_id):
@ -780,10 +780,10 @@ class BomList(generics.ListCreateAPIView):
kwargs['sub_part_detail'] = str2bool(self.request.GET.get('sub_part_detail', None))
except AttributeError:
pass
# Ensure the request context is passed through!
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
@ -867,7 +867,7 @@ class BomList(generics.ListCreateAPIView):
# Work out which lines have actually been validated
pks = []
for bom_item in queryset.all():
if bom_item.is_line_valid:
pks.append(bom_item.pk)
@ -915,7 +915,7 @@ class BomItemValidate(generics.UpdateAPIView):
valid = request.data.get('valid', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
@ -949,7 +949,7 @@ part_api_urls = [
url(r'^sale-price/', include([
url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
])),
# Base URL for PartParameter API endpoints
url(r'^parameter/', include([
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),

View File

@ -43,7 +43,7 @@ class PartConfig(AppConfig):
if part.image:
url = part.image.thumbnail.name
loc = os.path.join(settings.MEDIA_ROOT, url)
if not os.path.exists(loc):
logger.info("InvenTree: Generating thumbnail for Part '{p}'".format(p=part.name))
try:

View File

@ -69,7 +69,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
for item in items:
item.level = str(int(level))
# Avoid circular BOM references
if item.pk in uids:
continue
@ -79,7 +79,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
if item.sub_part.assembly:
if max_levels is None or level < max_levels:
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
if cascade:
# Cascading (multi-level) BOM
@ -124,7 +124,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
parameter_cols[name].update({b_idx: value})
except KeyError:
parameter_cols[name] = {b_idx: value}
# Add parameter columns to dataset
parameter_cols_ordered = OrderedDict(sorted(parameter_cols.items(), key=lambda x: x[0]))
add_columns_to_dataset(parameter_cols_ordered, len(bom_items))
@ -185,7 +185,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Filter manufacturer parts
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk)
manufacturer_parts = manufacturer_parts.prefetch_related('supplier_parts')
# Process manufacturer part
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts):
@ -250,7 +250,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Filter supplier parts
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk)
for idx, manufacturer_part in enumerate(manufacturer_parts):
if manufacturer_part:
@ -295,7 +295,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Filter supplier parts
supplier_parts = SupplierPart.objects.filter(part__pk=b_part.pk)
for idx, supplier_part in enumerate(supplier_parts):
if supplier_part.supplier:
@ -326,7 +326,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
filename = '{n}_BOM.{fmt}'.format(n=part.full_name, fmt=fmt)
return DownloadFile(data, filename)
class BomUploadManager:
""" Class for managing an uploaded BOM file """
@ -342,7 +342,7 @@ class BomUploadManager:
'Part_IPN',
'Part_ID',
]
# Fields which would be helpful but are not required
OPTIONAL_HEADERS = [
'Reference',
@ -360,7 +360,7 @@ class BomUploadManager:
def __init__(self, bom_file):
""" Initialize the BomUpload class with a user-uploaded file object """
self.process(bom_file)
def process(self, bom_file):
@ -387,7 +387,7 @@ class BomUploadManager:
def guess_header(self, header, threshold=80):
""" Try to match a header (from the file) to a list of known headers
Args:
header - Header name to look for
threshold - Match threshold for fuzzy search
@ -421,7 +421,7 @@ class BomUploadManager:
return matches[0]['header']
return None
def columns(self):
""" Return a list of headers for the thingy """
headers = []

View File

@ -82,7 +82,7 @@
tree_id: 2
lft: 1
rght: 4
- model: part.partcategory
pk: 8
fields:

View File

@ -95,11 +95,11 @@ class BomExportForm(forms.Form):
parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM"))
stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM"))
manufacturer_data = forms.BooleanField(label=_("Include Manufacturer Data"), required=False, initial=True, help_text=_("Include part manufacturer data in exported BOM"))
supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM"))
def get_choices(self):
""" BOM export format choices """
@ -324,7 +324,7 @@ class EditCategoryParameterTemplateForm(HelperForm):
add_to_all_categories = forms.BooleanField(required=False,
initial=False,
help_text=_('Add parameter template to all categories'))
class Meta:
model = PartCategoryParameterTemplate
fields = [

View File

@ -349,7 +349,7 @@ class Part(MPTTModel):
context['available'] = self.available_stock
context['on_order'] = self.on_order
context['required'] = context['required_build_order_quantity'] + context['required_sales_order_quantity']
context['allocated'] = context['allocated_build_order_quantity'] + context['allocated_sales_order_quantity']
@ -434,7 +434,7 @@ class Part(MPTTModel):
a) The parent part is the same as this one
b) The parent part is used in the BOM for *this* part
c) The parent part is used in the BOM for any child parts under this one
Failing this check raises a ValidationError!
"""
@ -506,7 +506,7 @@ class Part(MPTTModel):
parts = Part.objects.filter(tree_id=self.tree_id)
stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None)
# There are no matchin StockItem objects (skip further tests)
if not stock.exists():
return None
@ -578,7 +578,7 @@ class Part(MPTTModel):
if self.IPN:
elements.append(self.IPN)
elements.append(self.name)
if self.revision:
@ -663,7 +663,7 @@ class Part(MPTTModel):
def clean(self):
"""
Perform cleaning operations for the Part model
Update trackable status:
If this part is trackable, and it is used in the BOM
for a parent part which is *not* trackable,
@ -946,7 +946,7 @@ class Part(MPTTModel):
quantity = 0
for build in builds:
bom_item = None
# List the bom lines required to make the build (including inherited ones!)
@ -958,7 +958,7 @@ class Part(MPTTModel):
build_quantity = build.quantity * bom_item.quantity
quantity += build_quantity
return quantity
def requiring_sales_orders(self):
@ -1008,7 +1008,7 @@ class Part(MPTTModel):
def quantity_to_order(self):
"""
Return the quantity needing to be ordered for this part.
Here, an "order" could be one of:
- Build Order
- Sales Order
@ -1019,7 +1019,7 @@ class Part(MPTTModel):
Required for orders = self.required_order_quantity()
Currently on order = self.on_order
Currently building = self.quantity_being_built
"""
# Total requirement
@ -1114,7 +1114,7 @@ class Part(MPTTModel):
if total is None:
total = 0
return max(total, 0)
@property
@ -1238,7 +1238,7 @@ class Part(MPTTModel):
@property
def total_stock(self):
""" Return the total stock quantity for this part.
- Part may be stored in multiple locations
- If this part is a "template" (variants exist) then these are counted too
"""
@ -1463,7 +1463,7 @@ class Part(MPTTModel):
# Start with a list of all parts designated as 'sub components'
parts = Part.objects.filter(component=True)
# Exclude this part
parts = parts.exclude(id=self.id)
@ -1496,7 +1496,7 @@ class Part(MPTTModel):
def get_price_info(self, quantity=1, buy=True, bom=True):
""" Return a simplified pricing string for this part
Args:
quantity: Number of units to calculate price for
buy: Include supplier pricing (default = True)
@ -1519,7 +1519,7 @@ class Part(MPTTModel):
return "{a} - {b}".format(a=min_price, b=max_price)
def get_supplier_price_range(self, quantity=1):
min_price = None
max_price = None
@ -1586,7 +1586,7 @@ class Part(MPTTModel):
return (min_price, max_price)
def get_price_range(self, quantity=1, buy=True, bom=True):
""" Return the price range for this part. This price can be either:
- Supplier price (if purchased from suppliers)
@ -1683,7 +1683,7 @@ class Part(MPTTModel):
@transaction.atomic
def copy_parameters_from(self, other, **kwargs):
clear = kwargs.get('clear', True)
if clear:
@ -1730,7 +1730,7 @@ class Part(MPTTModel):
# Copy the parameters data
if kwargs.get('parameters', True):
self.copy_parameters_from(other)
# Copy the fields that aren't available in the duplicate form
self.salable = other.salable
self.assembly = other.assembly
@ -1760,7 +1760,7 @@ class Part(MPTTModel):
tests = tests.filter(required=required)
return tests
def getRequiredTests(self):
# Return the tests which are required by this part
return self.getTestTemplates(required=True)
@ -1906,7 +1906,7 @@ class PartAttachment(InvenTreeAttachment):
"""
Model for storing file attachments against a Part object
"""
def getSubdir(self):
return os.path.join("part_files", str(self.part.id))
@ -2265,7 +2265,7 @@ class BomItem(models.Model):
def validate_hash(self, valid=True):
""" Mark this item as 'valid' (store the checksum hash).
Args:
valid: If true, validate the hash, otherwise invalidate it (default = True)
"""
@ -2303,7 +2303,7 @@ class BomItem(models.Model):
# Check for circular BOM references
if self.sub_part:
self.sub_part.checkAddToBOM(self.part)
# If the sub_part is 'trackable' then the 'quantity' field must be an integer
if self.sub_part.trackable:
if not self.quantity == int(self.quantity):
@ -2339,7 +2339,7 @@ class BomItem(models.Model):
"""
query = self.sub_part.stock_items.all()
query = query.prefetch_related([
'sub_part__stock_items',
])
@ -2396,7 +2396,7 @@ class BomItem(models.Model):
def get_required_quantity(self, build_quantity):
""" Calculate the required part quantity, based on the supplier build_quantity.
Includes overage estimate in the returned value.
Args:
build_quantity: Number of parts to build

View File

@ -134,7 +134,7 @@ class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
stock = serializers.FloatField(source='total_stock')
class Meta:
@ -232,7 +232,7 @@ class PartSerializer(InvenTreeModelSerializer):
output_field=models.DecimalField(),
)
)
# Filter to limit orders to "open"
order_filter = Q(
order__status__in=PurchaseOrderStatus.OPEN
@ -259,7 +259,7 @@ class PartSerializer(InvenTreeModelSerializer):
output_field=models.DecimalField(),
),
)
return queryset
def get_starred(self, part):
@ -358,7 +358,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField()
part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True))
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(component=True))

View File

@ -46,5 +46,5 @@
part: {{ part.id }},
}
});
{% endblock %}

View File

@ -198,7 +198,7 @@
})
$("#part-export").click(function() {
var url = "{% url 'part-export' %}?category={{ category.id }}";
location.href = url;

View File

@ -20,20 +20,20 @@
<tr>
<td><span class='fas fa-font'></span></td>
<td><strong>{% trans "Part name" %}</strong></td>
<td>{{ part.name }}</td>
<td>{{ part.name }}{% include "clip.html"%}</td>
</tr>
{% if part.IPN %}
<tr>
<td></td>
<td><strong>{% trans "IPN" %}</strong></td>
<td>{{ part.IPN }}</td>
<td>{{ part.IPN }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.revision %}
<tr>
<td><span class='fas fa-code-branch'></span></td>
<td><strong>{% trans "Revision" %}</strong></td>
<td>{{ part.revision }}</td>
<td>{{ part.revision }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.trackable %}
@ -42,7 +42,7 @@
<td><strong>{% trans "Latest Serial Number" %}</strong></td>
<td>
{% if part.getLatestSerialNumber %}
{{ part.getLatestSerialNumber }}
{{ part.getLatestSerialNumber }}{% include "clip.html"%}
{% else %}
<em>{% trans "No serial numbers recorded" %}</em>
{% endif %}
@ -52,7 +52,7 @@
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td><strong>{% trans "Description" %}</strong></td>
<td>{{ part.description }}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
{% if part.variant_of %}
<tr>
@ -96,7 +96,7 @@
<td></td>
<td><strong>{% trans "Default Supplier" %}</strong></td>
<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>
</tr>
{% endif %}
@ -262,5 +262,5 @@
},
);
});
{% endblock %}

View File

@ -58,7 +58,7 @@
});
$("#manufacturer-part-delete").click(function() {
var selections = $("#manufacturer-table").bootstrapTable("getSelections");
var parts = [];

View File

@ -20,12 +20,12 @@
{% if editing %}
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
</form>
{{ form.media }}

View File

@ -49,7 +49,7 @@
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -151,7 +151,7 @@
<td>{% decimal allocated %}</td>
</tr>
{% endif %}
{% if not part.is_template %}
{% if part.assembly %}
<tr>
@ -177,11 +177,11 @@
</table>
</div>
</div>
</div>
<div class='panel panel-default panel-inventree'>
<div class='panel-heading'>
<h4>
{% block heading %}
@ -272,7 +272,7 @@
function onSelectImage(response) {
// Callback when the image-selection modal form is displayed
// Populate the form with image data (requested via AJAX)
$("#modal-form").find("#image-select-table").bootstrapTable({
pagination: true,
pageSize: 25,
@ -301,9 +301,9 @@
}
});
}
{% if roles.part.change %}
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
{% if allow_download %}
$("#part-image-url").click(function() {

View File

@ -11,7 +11,7 @@
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
{% csrf_token %}
{% load crispy_forms_tags %}
<input id='image-input' name='image' type='hidden' value="{{ part.image }}">
<table id='image-select-table' class='table table-striped table-condensed table-img-grid'>

View File

@ -4,10 +4,10 @@
{% block form %}
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
{% load crispy_forms_tags %}
<label class='control-label'>Parts</label>
<p class='help-block'>{% trans "Set category for the following parts" %}</p>
<table class='table table-striped'>
<tr>
<th>{% trans "Part" %}</th>
@ -36,8 +36,8 @@
</tr>
{% endfor %}
</table>
{% crispy form %}
</form>
{% endblock %}

View File

@ -11,7 +11,7 @@
{% block category_content %}
<div class='panel panel-default panel-inventree'>
<div class='panel-heading'>
<h4>{% trans "Subcategories" %}</h4>
</div>

Some files were not shown because too many files have changed in this diff Show More