Merge branch 'master' of https://github.com/inventree/InvenTree into order-modal-show-price

This commit is contained in:
Matthias 2021-06-22 00:27:56 +02:00
commit b503c62464
32 changed files with 740 additions and 35 deletions

View File

@ -33,6 +33,7 @@ def canAppAccessDatabase():
'createsuperuser',
'wait_for_db',
'prerender',
'rebuild',
'collectstatic',
'makemessages',
'compilemessages',

View File

@ -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(

View File

@ -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():

View File

@ -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)

View File

@ -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

View File

@ -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 """

View File

@ -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')},
},
),
]

View File

@ -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)

View File

@ -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 """

View File

@ -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 %}

View File

@ -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 = [

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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 '''

View File

@ -706,6 +706,7 @@ class PartList(generics.ListCreateAPIView):
'creation_date',
'IPN',
'in_stock',
'category',
]
# Default ordering

View File

@ -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():

View File

@ -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>

View File

@ -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 """

View File

@ -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:

View File

@ -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 """

View 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)
]

View File

@ -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',

View File

@ -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>

View File

@ -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>

View File

@ -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}/`);
}

View File

@ -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) {

View File

@ -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" %}',

View File

@ -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 %}

View File

@ -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
View 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)

View File

@ -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):
"""