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 order-modal-show-price
This commit is contained in:
commit
b503c62464
@ -33,6 +33,7 @@ def canAppAccessDatabase():
|
||||
'createsuperuser',
|
||||
'wait_for_db',
|
||||
'prerender',
|
||||
'rebuild',
|
||||
'collectstatic',
|
||||
'makemessages',
|
||||
'compilemessages',
|
||||
|
@ -4,9 +4,10 @@ Provides system status functionality checks.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
from django_q.models import Success
|
||||
from django_q.monitor import Stat
|
||||
@ -34,7 +35,7 @@ def is_worker_running(**kwargs):
|
||||
Check to see if we have a result within the last 20 minutes
|
||||
"""
|
||||
|
||||
now = datetime.now()
|
||||
now = timezone.now()
|
||||
past = now - timedelta(minutes=20)
|
||||
|
||||
results = Success.objects.filter(
|
||||
|
@ -20,9 +20,12 @@ v4 -> 2021-06-01
|
||||
- BOM items can now accept "variant stock" to be assigned against them
|
||||
- Many slight API tweaks were needed to get this to work properly!
|
||||
|
||||
v5 -> 2021-06-21
|
||||
- Adds API interface for manufacturer part parameters
|
||||
|
||||
"""
|
||||
|
||||
INVENTREE_API_VERSION = 4
|
||||
INVENTREE_API_VERSION = 5
|
||||
|
||||
|
||||
def inventreeInstanceName():
|
||||
|
@ -11,6 +11,7 @@ import import_export.widgets as widgets
|
||||
from .models import Company
|
||||
from .models import SupplierPart
|
||||
from .models import SupplierPriceBreak
|
||||
from .models import ManufacturerPart, ManufacturerPartParameter
|
||||
|
||||
from part.models import Part
|
||||
|
||||
@ -71,6 +72,92 @@ class SupplierPartAdmin(ImportExportModelAdmin):
|
||||
]
|
||||
|
||||
|
||||
class ManufacturerPartResource(ModelResource):
|
||||
"""
|
||||
Class for managing ManufacturerPart data import/export
|
||||
"""
|
||||
|
||||
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
|
||||
|
||||
part_name = Field(attribute='part__full_name', readonly=True)
|
||||
|
||||
manufacturer = Field(attribute='manufacturer', widget=widgets.ForeignKeyWidget(Company))
|
||||
|
||||
manufacturer_name = Field(attribute='manufacturer__name', readonly=True)
|
||||
|
||||
class Meta:
|
||||
model = ManufacturerPart
|
||||
skip_unchanged = True
|
||||
report_skipped = True
|
||||
clean_model_instances = True
|
||||
|
||||
|
||||
class ManufacturerPartParameterInline(admin.TabularInline):
|
||||
"""
|
||||
Inline for editing ManufacturerPartParameter objects,
|
||||
directly from the ManufacturerPart admin view.
|
||||
"""
|
||||
|
||||
model = ManufacturerPartParameter
|
||||
|
||||
|
||||
class SupplierPartInline(admin.TabularInline):
|
||||
"""
|
||||
Inline for the SupplierPart model
|
||||
"""
|
||||
|
||||
model = SupplierPart
|
||||
|
||||
|
||||
class ManufacturerPartAdmin(ImportExportModelAdmin):
|
||||
"""
|
||||
Admin class for ManufacturerPart model
|
||||
"""
|
||||
|
||||
resource_class = ManufacturerPartResource
|
||||
|
||||
list_display = ('part', 'manufacturer', 'MPN')
|
||||
|
||||
search_fields = [
|
||||
'manufacturer__name',
|
||||
'part__name',
|
||||
'MPN',
|
||||
]
|
||||
|
||||
inlines = [
|
||||
SupplierPartInline,
|
||||
ManufacturerPartParameterInline,
|
||||
]
|
||||
|
||||
|
||||
class ManufacturerPartParameterResource(ModelResource):
|
||||
"""
|
||||
Class for managing ManufacturerPartParameter data import/export
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ManufacturerPartParameter
|
||||
skip_unchanged = True
|
||||
report_skipped = True
|
||||
clean_model_instance = True
|
||||
|
||||
|
||||
class ManufacturerPartParameterAdmin(ImportExportModelAdmin):
|
||||
"""
|
||||
Admin class for ManufacturerPartParameter model
|
||||
"""
|
||||
|
||||
resource_class = ManufacturerPartParameterResource
|
||||
|
||||
list_display = ('manufacturer_part', 'name', 'value')
|
||||
|
||||
search_fields = [
|
||||
'manufacturer_part__manufacturer__name',
|
||||
'name',
|
||||
'value'
|
||||
]
|
||||
|
||||
|
||||
class SupplierPriceBreakResource(ModelResource):
|
||||
""" Class for managing SupplierPriceBreak data import/export """
|
||||
|
||||
@ -103,3 +190,6 @@ class SupplierPriceBreakAdmin(ImportExportModelAdmin):
|
||||
admin.site.register(Company, CompanyAdmin)
|
||||
admin.site.register(SupplierPart, SupplierPartAdmin)
|
||||
admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin)
|
||||
|
||||
admin.site.register(ManufacturerPart, ManufacturerPartAdmin)
|
||||
admin.site.register(ManufacturerPartParameter, ManufacturerPartParameterAdmin)
|
||||
|
@ -15,11 +15,11 @@ from django.db.models import Q
|
||||
from InvenTree.helpers import str2bool
|
||||
|
||||
from .models import Company
|
||||
from .models import ManufacturerPart
|
||||
from .models import ManufacturerPart, ManufacturerPartParameter
|
||||
from .models import SupplierPart, SupplierPriceBreak
|
||||
|
||||
from .serializers import CompanySerializer
|
||||
from .serializers import ManufacturerPartSerializer
|
||||
from .serializers import ManufacturerPartSerializer, ManufacturerPartParameterSerializer
|
||||
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
|
||||
|
||||
|
||||
@ -175,6 +175,86 @@ class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = ManufacturerPartSerializer
|
||||
|
||||
|
||||
class ManufacturerPartParameterList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for list view of ManufacturerPartParamater model.
|
||||
"""
|
||||
|
||||
queryset = ManufacturerPartParameter.objects.all()
|
||||
serializer_class = ManufacturerPartParameterSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
# Do we wish to include any extra detail?
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
optional_fields = [
|
||||
'manufacturer_part_detail',
|
||||
]
|
||||
|
||||
for key in optional_fields:
|
||||
kwargs[key] = str2bool(params.get(key, None))
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering for the queryset
|
||||
"""
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# Filter by manufacturer?
|
||||
manufacturer = params.get('manufacturer', None)
|
||||
|
||||
if manufacturer is not None:
|
||||
queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer)
|
||||
|
||||
# Filter by part?
|
||||
part = params.get('part', None)
|
||||
|
||||
if part is not None:
|
||||
queryset = queryset.filter(manufacturer_part__part=part)
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
filters.SearchFilter,
|
||||
filters.OrderingFilter,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'name',
|
||||
'value',
|
||||
'units',
|
||||
'manufacturer_part',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'name',
|
||||
'value',
|
||||
'units',
|
||||
]
|
||||
|
||||
|
||||
class ManufacturerPartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of ManufacturerPartParameter model
|
||||
"""
|
||||
|
||||
queryset = ManufacturerPartParameter.objects.all()
|
||||
serializer_class = ManufacturerPartParameterSerializer
|
||||
|
||||
|
||||
class SupplierPartList(generics.ListCreateAPIView):
|
||||
""" API endpoint for list view of SupplierPart object
|
||||
|
||||
@ -249,7 +329,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
||||
params = self.request.query_params
|
||||
kwargs['part_detail'] = str2bool(params.get('part_detail', None))
|
||||
kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', None))
|
||||
kwargs['manufacturer_detail'] = str2bool(self.params.get('manufacturer_detail', None))
|
||||
kwargs['manufacturer_detail'] = str2bool(params.get('manufacturer_detail', None))
|
||||
kwargs['pretty'] = str2bool(params.get('pretty', None))
|
||||
except AttributeError:
|
||||
pass
|
||||
@ -316,6 +396,13 @@ class SupplierPriceBreakList(generics.ListCreateAPIView):
|
||||
|
||||
manufacturer_part_api_urls = [
|
||||
|
||||
url(r'^parameter/', include([
|
||||
url(r'^(?P<pk>\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'),
|
||||
|
||||
# Catch anything else
|
||||
url(r'^.*$', ManufacturerPartParameterList.as_view(), name='api-manufacturer-part-parameter-list'),
|
||||
])),
|
||||
|
||||
url(r'^(?P<pk>\d+)/?', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'),
|
||||
|
||||
# Catch anything else
|
||||
|
@ -16,7 +16,7 @@ from djmoney.forms.fields import MoneyField
|
||||
|
||||
from common.settings import currency_code_default
|
||||
|
||||
from .models import Company
|
||||
from .models import Company, ManufacturerPartParameter
|
||||
from .models import ManufacturerPart
|
||||
from .models import SupplierPart
|
||||
from .models import SupplierPriceBreak
|
||||
@ -105,6 +105,21 @@ class EditManufacturerPartForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class EditManufacturerPartParameterForm(HelperForm):
|
||||
"""
|
||||
Form for creating / editing a ManufacturerPartParameter object
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ManufacturerPartParameter
|
||||
fields = [
|
||||
'manufacturer_part',
|
||||
'name',
|
||||
'value',
|
||||
'units',
|
||||
]
|
||||
|
||||
|
||||
class EditSupplierPartForm(HelperForm):
|
||||
""" Form for editing a SupplierPart object """
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-20 07:48
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0037_supplierpart_update_3'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ManufacturerPartParameter',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Parameter name', max_length=500, verbose_name='Name')),
|
||||
('value', models.CharField(help_text='Parameter value', max_length=500, verbose_name='Value')),
|
||||
('units', models.CharField(blank=True, help_text='Parameter units', max_length=64, null=True, verbose_name='Units')),
|
||||
('manufacturer_part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameters', to='company.manufacturerpart', verbose_name='Manufacturer Part')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('manufacturer_part', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
@ -371,6 +371,47 @@ class ManufacturerPart(models.Model):
|
||||
return s
|
||||
|
||||
|
||||
class ManufacturerPartParameter(models.Model):
|
||||
"""
|
||||
A ManufacturerPartParameter represents a key:value parameter for a MnaufacturerPart.
|
||||
|
||||
This is used to represent parmeters / properties for a particular manufacturer part.
|
||||
|
||||
Each parameter is a simple string (text) value.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
unique_together = ('manufacturer_part', 'name')
|
||||
|
||||
manufacturer_part = models.ForeignKey(
|
||||
ManufacturerPart,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='parameters',
|
||||
verbose_name=_('Manufacturer Part'),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=500,
|
||||
blank=False,
|
||||
verbose_name=_('Name'),
|
||||
help_text=_('Parameter name')
|
||||
)
|
||||
|
||||
value = models.CharField(
|
||||
max_length=500,
|
||||
blank=False,
|
||||
verbose_name=_('Value'),
|
||||
help_text=_('Parameter value')
|
||||
)
|
||||
|
||||
units = models.CharField(
|
||||
max_length=64,
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Units'),
|
||||
help_text=_('Parameter units')
|
||||
)
|
||||
|
||||
|
||||
class SupplierPart(models.Model):
|
||||
""" Represents a unique part as provided by a Supplier
|
||||
Each SupplierPart is identified by a SKU (Supplier Part Number)
|
||||
|
@ -7,7 +7,7 @@ from rest_framework import serializers
|
||||
from sql_util.utils import SubqueryCount
|
||||
|
||||
from .models import Company
|
||||
from .models import ManufacturerPart
|
||||
from .models import ManufacturerPart, ManufacturerPartParameter
|
||||
from .models import SupplierPart, SupplierPriceBreak
|
||||
|
||||
from InvenTree.serializers import InvenTreeModelSerializer
|
||||
@ -124,6 +124,35 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ManufacturerPartParameterSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializer for the ManufacturerPartParameter model
|
||||
"""
|
||||
|
||||
manufacturer_part_detail = ManufacturerPartSerializer(source='manufacturer_part', many=False, read_only=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
man_detail = kwargs.pop('manufacturer_part_detail', False)
|
||||
|
||||
super(ManufacturerPartParameterSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
if not man_detail:
|
||||
self.fields.pop('manufacturer_part_detail')
|
||||
|
||||
class Meta:
|
||||
model = ManufacturerPartParameter
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'manufacturer_part',
|
||||
'manufacturer_part_detail',
|
||||
'name',
|
||||
'value',
|
||||
'units',
|
||||
]
|
||||
|
||||
|
||||
class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for SupplierPart object """
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Supplier Parts" %}
|
||||
{% trans "Suppliers" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
@ -30,9 +30,44 @@
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block post_content_panels %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Parameters" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='parameter-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-success' id='parameter-create'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
|
||||
</button>
|
||||
<div id='param-dropdown' class='btn-group'>
|
||||
<!-- TODO -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='parameter-table' data-toolbar='#parameter-toolbar'></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#parameter-create').click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'manufacturer-part-parameter-create' %}",
|
||||
{
|
||||
data: {
|
||||
manufacturer_part: {{ part.id }},
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#supplier-create').click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'supplier-part-create' %}",
|
||||
@ -84,6 +119,16 @@ loadSupplierPartTable(
|
||||
}
|
||||
);
|
||||
|
||||
loadManufacturerPartParameterTable(
|
||||
"#parameter-table",
|
||||
"{% url 'api-manufacturer-part-parameter-list' %}",
|
||||
{
|
||||
params: {
|
||||
manufacturer_part: {{ part.id }},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
|
||||
|
||||
{% endblock %}
|
@ -53,20 +53,25 @@ price_break_urls = [
|
||||
url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
|
||||
]
|
||||
|
||||
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'),
|
||||
]
|
||||
|
||||
manufacturer_part_urls = [
|
||||
url(r'^new/?', views.ManufacturerPartCreate.as_view(), name='manufacturer-part-create'),
|
||||
|
||||
url(r'delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'),
|
||||
url(r'^delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/', include(manufacturer_part_detail_urls)),
|
||||
# URLs for ManufacturerPartParameter views (create / edit / delete)
|
||||
url(r'^parameter/', include([
|
||||
url(r'^new/', views.ManufacturerPartParameterCreate.as_view(), name='manufacturer-part-parameter-create'),
|
||||
url(r'^(?P<pk>\d)/', include([
|
||||
url(r'^edit/', views.ManufacturerPartParameterEdit.as_view(), name='manufacturer-part-parameter-edit'),
|
||||
url(r'^delete/', views.ManufacturerPartParameterDelete.as_view(), name='manufacturer-part-parameter-delete'),
|
||||
])),
|
||||
])),
|
||||
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
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'),
|
||||
])),
|
||||
]
|
||||
|
||||
supplier_part_detail_urls = [
|
||||
|
@ -23,14 +23,14 @@ from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.views import InvenTreeRoleMixin
|
||||
|
||||
from .models import Company
|
||||
from .models import Company, ManufacturerPartParameter
|
||||
from .models import ManufacturerPart
|
||||
from .models import SupplierPart
|
||||
from .models import SupplierPriceBreak
|
||||
|
||||
from part.models import Part
|
||||
|
||||
from .forms import EditCompanyForm
|
||||
from .forms import EditCompanyForm, EditManufacturerPartParameterForm
|
||||
from .forms import CompanyImageForm
|
||||
from .forms import EditManufacturerPartForm
|
||||
from .forms import EditSupplierPartForm
|
||||
@ -504,6 +504,66 @@ class ManufacturerPartDelete(AjaxDeleteView):
|
||||
return self.renderJsonResponse(self.request, data=data, form=self.get_form())
|
||||
|
||||
|
||||
class ManufacturerPartParameterCreate(AjaxCreateView):
|
||||
"""
|
||||
View for creating a new ManufacturerPartParameter object
|
||||
"""
|
||||
|
||||
model = ManufacturerPartParameter
|
||||
form_class = EditManufacturerPartParameterForm
|
||||
ajax_form_title = _('Add Manufacturer Part Parameter')
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
# Hide the manufacturer_part field if specified
|
||||
if form.initial.get('manufacturer_part', None):
|
||||
form.fields['manufacturer_part'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial().copy()
|
||||
|
||||
manufacturer_part = self.get_param('manufacturer_part')
|
||||
|
||||
if manufacturer_part:
|
||||
try:
|
||||
initials['manufacturer_part'] = ManufacturerPartParameter.objects.get(pk=manufacturer_part)
|
||||
except (ValueError, ManufacturerPartParameter.DoesNotExist):
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
|
||||
class ManufacturerPartParameterEdit(AjaxUpdateView):
|
||||
"""
|
||||
View for editing a ManufacturerPartParameter object
|
||||
"""
|
||||
|
||||
model = ManufacturerPartParameter
|
||||
form_class = EditManufacturerPartParameterForm
|
||||
ajax_form_title = _('Edit Manufacturer Part Parameter')
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
form.fields['manufacturer_part'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class ManufacturerPartParameterDelete(AjaxDeleteView):
|
||||
"""
|
||||
View for deleting a ManufacturerPartParameter object
|
||||
"""
|
||||
|
||||
model = ManufacturerPartParameter
|
||||
|
||||
|
||||
class SupplierPartDetail(DetailView):
|
||||
""" Detail view for SupplierPart """
|
||||
model = SupplierPart
|
||||
@ -563,6 +623,7 @@ class SupplierPartEdit(AjaxUpdateView):
|
||||
supplier_part = self.get_object()
|
||||
|
||||
if supplier_part.manufacturer_part:
|
||||
if supplier_part.manufacturer_part.manufacturer:
|
||||
initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
|
||||
initials['MPN'] = supplier_part.manufacturer_part.MPN
|
||||
|
||||
|
@ -75,7 +75,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Order Reference" %}</td>
|
||||
<td>{{ order.reference }}{% include "clip.html"%}</td>
|
||||
<td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
|
@ -77,7 +77,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Order Reference" %}</td>
|
||||
<td>{{ order.reference }}{% include "clip.html"%}</td>
|
||||
<td>{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
|
@ -111,6 +111,13 @@ class PartCategoryResource(ModelResource):
|
||||
PartCategory.objects.rebuild()
|
||||
|
||||
|
||||
class PartCategoryInline(admin.TabularInline):
|
||||
"""
|
||||
Inline for PartCategory model
|
||||
"""
|
||||
model = PartCategory
|
||||
|
||||
|
||||
class PartCategoryAdmin(ImportExportModelAdmin):
|
||||
|
||||
resource_class = PartCategoryResource
|
||||
@ -119,6 +126,10 @@ class PartCategoryAdmin(ImportExportModelAdmin):
|
||||
|
||||
search_fields = ('name', 'description')
|
||||
|
||||
inlines = [
|
||||
PartCategoryInline,
|
||||
]
|
||||
|
||||
|
||||
class PartRelatedAdmin(admin.ModelAdmin):
|
||||
''' Class to manage PartRelated objects '''
|
||||
|
@ -706,6 +706,7 @@ class PartList(generics.ListCreateAPIView):
|
||||
'creation_date',
|
||||
'IPN',
|
||||
'in_stock',
|
||||
'category',
|
||||
]
|
||||
|
||||
# Default ordering
|
||||
|
@ -380,7 +380,6 @@ class Part(MPTTModel):
|
||||
previous.image.delete(save=False)
|
||||
|
||||
self.clean()
|
||||
self.validate_unique()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@ -672,6 +671,8 @@ class Part(MPTTModel):
|
||||
|
||||
super().clean()
|
||||
|
||||
self.validate_unique()
|
||||
|
||||
if self.trackable:
|
||||
for part in self.get_used_in().all():
|
||||
|
||||
|
@ -137,7 +137,7 @@
|
||||
<hr>
|
||||
<h4>{% trans 'Stock Pricing' %}<i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part.
|
||||
The part single price is the current purchase price for that supplier part."></i></h4>
|
||||
{% if price_history|length > 1 %}
|
||||
{% if price_history|length > 0 %}
|
||||
<div style="max-width: 99%; min-height: 300px">
|
||||
<canvas id="StockPriceChart"></canvas>
|
||||
</div>
|
||||
|
@ -5,6 +5,7 @@ over and above the built-in Django tags.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings as djangosettings
|
||||
@ -114,6 +115,14 @@ def inventree_title(*args, **kwargs):
|
||||
return version.inventreeInstanceTitle()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def python_version(*args, **kwargs):
|
||||
"""
|
||||
Return the current python version
|
||||
"""
|
||||
return sys.version.split(' ')[0]
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_version(*args, **kwargs):
|
||||
""" Return InvenTree version string """
|
||||
|
@ -814,7 +814,7 @@ class PartPricingView(PartDetail):
|
||||
part = self.get_part()
|
||||
# Stock history
|
||||
if part.total_stock > 1:
|
||||
ret = []
|
||||
price_history = []
|
||||
stock = part.stock_entries(include_variants=False, in_stock=True) # .order_by('purchase_order__date')
|
||||
stock = stock.prefetch_related('purchase_order', 'supplier_part')
|
||||
|
||||
@ -841,9 +841,9 @@ class PartPricingView(PartDetail):
|
||||
line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y')
|
||||
else:
|
||||
line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y')
|
||||
ret.append(line)
|
||||
price_history.append(line)
|
||||
|
||||
ctx['price_history'] = ret
|
||||
ctx['price_history'] = price_history
|
||||
|
||||
# BOM Information for Pie-Chart
|
||||
if part.has_bom:
|
||||
|
@ -44,6 +44,13 @@ class LocationResource(ModelResource):
|
||||
StockLocation.objects.rebuild()
|
||||
|
||||
|
||||
class LocationInline(admin.TabularInline):
|
||||
"""
|
||||
Inline for sub-locations
|
||||
"""
|
||||
model = StockLocation
|
||||
|
||||
|
||||
class LocationAdmin(ImportExportModelAdmin):
|
||||
|
||||
resource_class = LocationResource
|
||||
@ -52,6 +59,10 @@ class LocationAdmin(ImportExportModelAdmin):
|
||||
|
||||
search_fields = ('name', 'description')
|
||||
|
||||
inlines = [
|
||||
LocationInline,
|
||||
]
|
||||
|
||||
|
||||
class StockItemResource(ModelResource):
|
||||
""" Class for managing StockItem data import/export """
|
||||
|
76
InvenTree/stock/migrations/0064_auto_20210621_1724.py
Normal file
76
InvenTree/stock/migrations/0064_auto_20210621_1724.py
Normal file
@ -0,0 +1,76 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-21 07:24
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def extract_purchase_price(apps, schema_editor):
|
||||
"""
|
||||
Find instances of StockItem which do *not* have a purchase price set,
|
||||
but which point to a PurchaseOrder where there *is* a purchase price set.
|
||||
|
||||
Then, assign *that* purchase price to original StockItem.
|
||||
|
||||
This is to address an issue where older versions of InvenTree
|
||||
did not correctly copy purchase price information cross to the StockItem objects.
|
||||
|
||||
Current InvenTree version (as of 2021-06-21) copy this information across correctly,
|
||||
so this one-time data migration should suffice.
|
||||
"""
|
||||
|
||||
# Required database models
|
||||
StockItem = apps.get_model('stock', 'stockitem')
|
||||
PurchaseOrder = apps.get_model('order', 'purchaseorder')
|
||||
PurchaseOrderLineItem = apps.get_model('order', 'purchaseorderlineitem')
|
||||
Part = apps.get_model('part', 'part')
|
||||
|
||||
# Find all the StockItem objects without a purchase_price which point to a PurchaseOrder
|
||||
items = StockItem.objects.filter(purchase_price=None).exclude(purchase_order=None)
|
||||
|
||||
print(f"Found {items.count()} stock items with missing purchase price information")
|
||||
|
||||
update_count = 0
|
||||
|
||||
for item in items:
|
||||
|
||||
part_id = item.part
|
||||
|
||||
po = item.purchase_order
|
||||
|
||||
# Look for a matching PurchaseOrderLineItem (with a price)
|
||||
lines = PurchaseOrderLineItem.objects.filter(part__part=part_id, order=po)
|
||||
|
||||
if lines.exists():
|
||||
|
||||
for line in lines:
|
||||
if line.purchase_price is not None:
|
||||
|
||||
# Copy pricing information across
|
||||
item.purchase_price = line.purchase_price
|
||||
item.purchases_price_currency = line.purchase_price_currency
|
||||
|
||||
print(f"- Updating supplier price for {item.part.name} - {item.purchase_price} {item.purchase_price_currency}")
|
||||
|
||||
update_count += 1
|
||||
|
||||
item.save()
|
||||
|
||||
break
|
||||
|
||||
print(f"Updated pricing for {update_count} stock items")
|
||||
|
||||
def reverse_operation(apps, schema_editor):
|
||||
"""
|
||||
DO NOTHING!
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0063_auto_20210511_2343'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(extract_purchase_price, reverse_code=reverse_operation)
|
||||
]
|
@ -81,6 +81,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
'belongs_to',
|
||||
'build',
|
||||
'customer',
|
||||
'purchase_order',
|
||||
'sales_order',
|
||||
'supplier_part',
|
||||
'supplier_part__supplier',
|
||||
@ -163,6 +164,10 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
purchase_price = serializers.SerializerMethodField()
|
||||
|
||||
purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True)
|
||||
|
||||
sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True)
|
||||
|
||||
def get_purchase_price(self, obj):
|
||||
""" Return purchase_price (Money field) as string (includes currency) """
|
||||
|
||||
@ -208,10 +213,13 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
'packaging',
|
||||
'part',
|
||||
'part_detail',
|
||||
'purchase_order',
|
||||
'purchase_order_reference',
|
||||
'pk',
|
||||
'quantity',
|
||||
'required_tests',
|
||||
'sales_order',
|
||||
'sales_order_reference',
|
||||
'serial',
|
||||
'stale',
|
||||
'status',
|
||||
|
@ -325,7 +325,7 @@
|
||||
<td><a href="{% url 'po-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.purchase_price %}
|
||||
{% if item.purchase_price != None %}
|
||||
<tr>
|
||||
<td><span class='fas fa-dollar-sign'></span></td>
|
||||
<td>{% trans "Purchase Price" %}</td>
|
||||
@ -350,7 +350,12 @@
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Manufacturer" %}</td>
|
||||
{% if item.supplier_part.manufacturer_part.manufacturer %}
|
||||
<td><a href="{% url 'company-detail' item.supplier_part.manufacturer_part.manufacturer.id %}">{{ item.supplier_part.manufacturer_part.manufacturer.name }}</a></td>
|
||||
{% else %}
|
||||
<td><i>{% trans "No manufacturer set" %}</i></td>
|
||||
{% endif %}
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
|
@ -34,6 +34,11 @@
|
||||
<td>{% trans "API Version" %}</td>
|
||||
<td>{% inventree_api_version %}{% include "clip.html" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Python Version" %}</td>
|
||||
<td>{% python_version %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Django Version" %}</td>
|
||||
|
@ -126,7 +126,7 @@ function loadManufacturerPartTable(table, url, options) {
|
||||
queryParams: filters,
|
||||
name: 'manufacturerparts',
|
||||
groupBy: false,
|
||||
formatNoMatches: function() { return "{% trans "No manufacturer parts found" %}"; },
|
||||
formatNoMatches: function() { return '{% trans "No manufacturer parts found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
checkbox: true,
|
||||
@ -199,6 +199,107 @@ function loadManufacturerPartTable(table, url, options) {
|
||||
}
|
||||
|
||||
|
||||
function loadManufacturerPartParameterTable(table, url, options) {
|
||||
/*
|
||||
* Load table of ManufacturerPartParameter objects
|
||||
*/
|
||||
|
||||
var params = options.params || {};
|
||||
|
||||
// Load filters
|
||||
var filters = loadTableFilters("manufacturer-part-parameters");
|
||||
|
||||
// Overwrite explicit parameters
|
||||
for (var key in params) {
|
||||
filters[key] = params[key];
|
||||
}
|
||||
|
||||
// setupFilterList("manufacturer-part-parameters", $(table));
|
||||
|
||||
$(table).inventreeTable({
|
||||
url: url,
|
||||
method: 'get',
|
||||
original: params,
|
||||
queryParams: filters,
|
||||
name: 'manufacturerpartparameters',
|
||||
groupBy: false,
|
||||
formatNoMatches: function() { return '{% trans "No parameters found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
checkbox: true,
|
||||
switchable: false,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '{% trans "Name" %}',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
title: '{% trans "Value" %}',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'units',
|
||||
title: '{% trans "Units" %}',
|
||||
switchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '',
|
||||
switchable: false,
|
||||
sortable: false,
|
||||
formatter: function(value, row) {
|
||||
|
||||
var pk = row.pk;
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-parameter-edit', pk, '{% trans "Edit parameter" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-parameter-delete', pk, '{% trans "Delete parameter" %}');
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
],
|
||||
onPostBody: function() {
|
||||
// Setup callback functions
|
||||
$(table).find('.button-parameter-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/manufacturer-part/parameter/${pk}/edit/`,
|
||||
{
|
||||
success: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
$(table).find('.button-parameter-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/manufacturer-part/parameter/${pk}/delete/`,
|
||||
{
|
||||
success: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadSupplierPartTable(table, url, options) {
|
||||
/*
|
||||
* Load supplier part table
|
||||
@ -224,7 +325,7 @@ function loadSupplierPartTable(table, url, options) {
|
||||
queryParams: filters,
|
||||
name: 'supplierparts',
|
||||
groupBy: false,
|
||||
formatNoMatches: function() { return "{% trans "No supplier parts found" %}"; },
|
||||
formatNoMatches: function() { return '{% trans "No supplier parts found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
checkbox: true,
|
||||
@ -260,7 +361,7 @@ function loadSupplierPartTable(table, url, options) {
|
||||
{
|
||||
sortable: true,
|
||||
field: 'supplier',
|
||||
title: "{% trans "Supplier" %}",
|
||||
title: '{% trans "Supplier" %}',
|
||||
formatter: function(value, row, index, field) {
|
||||
if (value) {
|
||||
var name = row.supplier_detail.name;
|
||||
@ -276,7 +377,7 @@ function loadSupplierPartTable(table, url, options) {
|
||||
{
|
||||
sortable: true,
|
||||
field: 'SKU',
|
||||
title: "{% trans "Supplier Part" %}",
|
||||
title: '{% trans "Supplier Part" %}',
|
||||
formatter: function(value, row, index, field) {
|
||||
return renderLink(value, `/supplier-part/${row.pk}/`);
|
||||
}
|
||||
|
@ -447,6 +447,7 @@ function loadPartTable(table, url, options={}) {
|
||||
|
||||
columns.push({
|
||||
sortable: true,
|
||||
sortName: 'category',
|
||||
field: 'category_detail',
|
||||
title: '{% trans "Category" %}',
|
||||
formatter: function(value, row, index, field) {
|
||||
|
@ -660,6 +660,27 @@ function loadStockTable(table, options) {
|
||||
title: '{% trans "Last Updated" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'purchase_order',
|
||||
title: '{% trans "Purchase Order" %}',
|
||||
formatter: function(value, row) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
var link = `/order/purchase-order/${row.purchase_order}/`;
|
||||
var text = `${row.purchase_order}`;
|
||||
|
||||
if (row.purchase_order_reference) {
|
||||
|
||||
var prefix = '{% settings_value "PURCHASEORDER_REFERENCE_PREFIX" %}';
|
||||
|
||||
text = prefix + row.purchase_order_reference;
|
||||
}
|
||||
|
||||
return renderLink(text, link);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'purchase_price',
|
||||
title: '{% trans "Purchase Price" %}',
|
||||
|
@ -43,6 +43,9 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pre_content_panels %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_panels %}
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
@ -63,6 +66,9 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block post_content_panels %}
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
|
@ -85,6 +85,7 @@ class RuleSet(models.Model):
|
||||
'part_partstar',
|
||||
'company_supplierpart',
|
||||
'company_manufacturerpart',
|
||||
'company_manufacturerpartparameter',
|
||||
],
|
||||
'stock_location': [
|
||||
'stock_stocklocation',
|
||||
@ -116,6 +117,8 @@ class RuleSet(models.Model):
|
||||
'order_purchaseorderattachment',
|
||||
'order_purchaseorderlineitem',
|
||||
'company_supplierpart',
|
||||
'company_manufacturerpart',
|
||||
'company_manufacturerpartparameter',
|
||||
],
|
||||
'sales_order': [
|
||||
'company_company',
|
||||
|
23
RELEASE.md
Normal file
23
RELEASE.md
Normal file
@ -0,0 +1,23 @@
|
||||
## Release Checklist
|
||||
|
||||
Checklist of steps to perform at each code release
|
||||
|
||||
### Update Version String
|
||||
|
||||
Update `INVENTREE_SW_VERSION` in [version.py](https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/version.py)
|
||||
|
||||
### Increment API Version
|
||||
|
||||
If the API has changed, ensure that the API version number is incremented.
|
||||
|
||||
### Translation Files
|
||||
|
||||
Merge the crowdin translation updates into master branch
|
||||
|
||||
### Documentation Release
|
||||
|
||||
Create new release for the [inventree documentation](https://github.com/inventree/inventree-docs)
|
||||
|
||||
### Python Library Release
|
||||
|
||||
Create new release for the [inventree python library](https://github.com/inventree/inventree-python)
|
18
tasks.py
18
tasks.py
@ -251,12 +251,15 @@ def content_excludes():
|
||||
"contenttypes",
|
||||
"sessions.session",
|
||||
"auth.permission",
|
||||
"authtoken.token",
|
||||
"error_report.error",
|
||||
"admin.logentry",
|
||||
"django_q.schedule",
|
||||
"django_q.task",
|
||||
"django_q.ormq",
|
||||
"users.owner",
|
||||
"exchange.rate",
|
||||
"exchange.exchangebackend",
|
||||
]
|
||||
|
||||
output = ""
|
||||
@ -362,6 +365,21 @@ def import_records(c, filename='data.json'):
|
||||
|
||||
print("Data import completed")
|
||||
|
||||
|
||||
@task
|
||||
def delete_data(c, force=False):
|
||||
"""
|
||||
Delete all database records!
|
||||
|
||||
Warning: This will REALLY delete all records in the database!!
|
||||
"""
|
||||
|
||||
if force:
|
||||
manage(c, 'flush --noinput')
|
||||
else:
|
||||
manage(c, 'flush')
|
||||
|
||||
|
||||
@task(post=[rebuild])
|
||||
def import_fixtures(c):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user