This commit is contained in:
Matthias 2021-12-02 17:24:07 +01:00
commit 0f0460f8ea
No known key found for this signature in database
GPG Key ID: F50EF5741D33E076
162 changed files with 65207 additions and 59759 deletions

View File

@ -0,0 +1,37 @@
name: Check Translations
on:
push:
branches:
- l10
pull_request:
branches:
- l10
jobs:
check:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INVENTREE_DB_NAME: './test_db.sqlite'
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
INVENTREE_DEBUG: info
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install gettext
pip3 install invoke
invoke install
- name: Test Translations
run: invoke translate
- name: Check Migration Files
run: python3 ci/check_migration_files.py

View File

@ -23,11 +23,13 @@ jobs:
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
steps:
- name: Install node.js
uses: actions/setup-node@v2
- run: npm install
- name: Checkout Code
uses: actions/checkout@v2
- name: Install node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- name: Setup Python
uses: actions/setup-python@v2
with:
@ -41,7 +43,6 @@ jobs:
invoke static
- name: Check HTML Files
run: |
npm install markuplint
npx markuplint InvenTree/build/templates/build/*.html
npx markuplint InvenTree/company/templates/company/*.html
npx markuplint InvenTree/order/templates/order/*.html

View File

@ -23,11 +23,13 @@ jobs:
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
steps:
- name: Install node.js
uses: actions/setup-node@v2
- run: npm install
- name: Checkout Code
uses: actions/checkout@v2
- name: Install node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- name: Setup Python
uses: actions/setup-python@v2
with:
@ -45,6 +47,5 @@ jobs:
python check_js_templates.py
- name: Lint Javascript Files
run: |
npm install eslint eslint-config-google
invoke render-js-files
npx eslint js_tmp/*.js

1
.gitignore vendored
View File

@ -78,5 +78,4 @@ locale_stats.json
# node.js
package-lock.json
package.json
node_modules/

View File

@ -46,7 +46,7 @@ class InvenTreeAPITestCase(APITestCase):
self.user.is_staff = True
self.user.save()
for role in self.roles:
self.assignRole(role)

View File

@ -1,9 +1,8 @@
"""
Pull rendered copies of the templated
Pull rendered copies of the templated
"""
from django.http import response
from django.test import TestCase, testcases
from django.test import TestCase
from django.contrib.auth import get_user_model
import os

View File

@ -53,7 +53,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
"""
Custom MoneyField for clean migrations while using dynamic currency settings
"""
def __init__(self, **kwargs):
# detect if creating migration
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:

View File

@ -38,7 +38,7 @@ class InvenTreeOrderingFilter(OrderingFilter):
ordering = []
for field in ordering_initial:
reverse = field.startswith('-')
if reverse:
@ -52,7 +52,7 @@ class InvenTreeOrderingFilter(OrderingFilter):
"""
Potentially, a single field could be "aliased" to multiple field,
(For example to enforce a particular ordering sequence)
e.g. to filter first by the integer value...

View File

@ -36,7 +36,7 @@ class Command(BaseCommand):
img = model.image
url = img.thumbnail.name
loc = os.path.join(settings.MEDIA_ROOT, url)
if not os.path.exists(loc):
logger.info(f"Generating thumbnail image for '{img}'")

View File

@ -31,7 +31,7 @@ class InvenTreeMetadata(SimpleMetadata):
"""
def determine_metadata(self, request, view):
self.request = request
self.view = view
@ -98,7 +98,7 @@ class InvenTreeMetadata(SimpleMetadata):
Override get_serializer_info so that we can add 'default' values
to any fields whose Meta.model specifies a default value
"""
serializer_info = super().get_serializer_info(serializer)
model_class = None
@ -174,7 +174,7 @@ class InvenTreeMetadata(SimpleMetadata):
# Extract extra information if an instance is available
if hasattr(serializer, 'instance'):
instance = serializer.instance
if instance is None and model_class is not None:
# Attempt to find the instance based on kwargs lookup
kwargs = getattr(self.view, 'kwargs', None)
@ -240,7 +240,7 @@ class InvenTreeMetadata(SimpleMetadata):
# Introspect writable related fields
if field_info['type'] == 'field' and not field_info['read_only']:
# If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset
if isinstance(field, serializers.PrimaryKeyRelatedField):
model = field.queryset.model

View File

@ -21,7 +21,8 @@ from django.dispatch import receiver
from mptt.models import MPTTModel, TreeForeignKey
from mptt.exceptions import InvalidMove
from .validators import validate_tree_name
from InvenTree.fields import InvenTreeURLField
from InvenTree.validators import validate_tree_name
logger = logging.getLogger('inventree')
@ -48,6 +49,9 @@ class ReferenceIndexingMixin(models.Model):
"""
A mixin for keeping track of numerical copies of the "reference" field.
!!DANGER!! always add `ReferenceIndexingSerializerMixin`to all your models serializers to
ensure the reference field is not too big
Here, we attempt to convert a "reference" field value (char) to an integer,
for performing fast natural sorting.
@ -68,33 +72,39 @@ class ReferenceIndexingMixin(models.Model):
reference = getattr(self, 'reference', '')
# Default value if we cannot convert to an integer
ref_int = 0
self.reference_int = extract_int(reference)
# Look at the start of the string - can it be "integerized"?
result = re.match(r"^(\d+)", reference)
reference_int = models.BigIntegerField(default=0)
if result and len(result.groups()) == 1:
ref = result.groups()[0]
try:
ref_int = int(ref)
except:
ref_int = 0
self.reference_int = ref_int
def extract_int(reference):
# Default value if we cannot convert to an integer
ref_int = 0
reference_int = models.IntegerField(default=0)
# Look at the start of the string - can it be "integerized"?
result = re.match(r"^(\d+)", reference)
if result and len(result.groups()) == 1:
ref = result.groups()[0]
try:
ref_int = int(ref)
except:
ref_int = 0
return ref_int
class InvenTreeAttachment(models.Model):
""" Provides an abstracted class for managing file attachments.
An attachment can be either an uploaded file, or an external URL
Attributes:
attachment: File
comment: String descriptor for the attachment
user: User associated with file upload
upload_date: Date the file was uploaded
"""
def getSubdir(self):
"""
Return the subdirectory under which attachments should be stored.
@ -103,11 +113,32 @@ class InvenTreeAttachment(models.Model):
return "attachments"
def save(self, *args, **kwargs):
# Either 'attachment' or 'link' must be specified!
if not self.attachment and not self.link:
raise ValidationError({
'attachment': _('Missing file'),
'link': _('Missing external link'),
})
super().save(*args, **kwargs)
def __str__(self):
return os.path.basename(self.attachment.name)
if self.attachment is not None:
return os.path.basename(self.attachment.name)
else:
return str(self.link)
attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'),
help_text=_('Select file to attach'))
help_text=_('Select file to attach'),
blank=True, null=True
)
link = InvenTreeURLField(
blank=True, null=True,
verbose_name=_('Link'),
help_text=_('Link to external URL')
)
comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment'))
@ -123,7 +154,10 @@ class InvenTreeAttachment(models.Model):
@property
def basename(self):
return os.path.basename(self.attachment.name)
if self.attachment:
return os.path.basename(self.attachment.name)
else:
return None
@basename.setter
def basename(self, fn):

View File

@ -16,6 +16,7 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import ugettext_lazy as _
from django.db import models
from djmoney.contrib.django_rest_framework.fields import MoneyField
from djmoney.money import Money
@ -27,6 +28,8 @@ from rest_framework.fields import empty
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import DecimalField
from .models import extract_int
class InvenTreeMoneySerializer(MoneyField):
"""
@ -66,7 +69,7 @@ class InvenTreeMoneySerializer(MoneyField):
if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty:
return Money(amount, currency)
return amount
@ -239,20 +242,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
return data
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
class ReferenceIndexingSerializerMixin():
"""
Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
The only real addition here is that we support "renaming" of the attachment file.
This serializer mixin ensures the the reference is not to big / small
for the BigIntegerField
"""
# The 'filename' field must be present in the serializer
filename = serializers.CharField(
label=_('Filename'),
required=False,
source='basename',
allow_blank=False,
)
def validate_reference(self, value):
if extract_int(value) > models.BigIntegerField.MAX_BIGINT:
raise serializers.ValidationError('reference is to to big')
return value
class InvenTreeAttachmentSerializerField(serializers.FileField):
@ -284,6 +282,27 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
return os.path.join(str(settings.MEDIA_URL), str(value))
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
"""
Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
The only real addition here is that we support "renaming" of the attachment file.
"""
attachment = InvenTreeAttachmentSerializerField(
required=False,
allow_null=False,
)
# The 'filename' field must be present in the serializer
filename = serializers.CharField(
label=_('Filename'),
required=False,
source='basename',
allow_blank=False,
)
class InvenTreeImageSerializerField(serializers.ImageField):
"""
Custom image serializer.

View File

@ -26,6 +26,7 @@ import moneyed
import yaml
from django.utils.translation import gettext_lazy as _
from django.contrib.messages import constants as messages
import django.conf.locale
def _is_true(x):
@ -256,7 +257,7 @@ INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'user_sessions', # db user sessions
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
@ -304,7 +305,7 @@ INSTALLED_APPS = [
MIDDLEWARE = CONFIG.get('middleware', [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'user_sessions.middleware.SessionMiddleware', # db user sessions
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@ -634,6 +635,12 @@ if _cache_host:
# as well
Q_CLUSTER["django_redis"] = "worker"
# database user sessions
SESSION_ENGINE = 'user_sessions.backends.db'
LOGOUT_REDIRECT_URL = 'index'
SILENCED_SYSTEM_CHECKS = [
'admin.E410',
]
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
@ -673,7 +680,7 @@ LANGUAGES = [
('el', _('Greek')),
('en', _('English')),
('es', _('Spanish')),
('es-mx', _('Spanish (Mexican')),
('es-mx', _('Spanish (Mexican)')),
('fr', _('French')),
('he', _('Hebrew')),
('it', _('Italian')),
@ -691,6 +698,25 @@ LANGUAGES = [
('zh-cn', _('Chinese')),
]
# Testing interface translations
if get_setting('TEST_TRANSLATIONS', False):
# Set default language
LANGUAGE_CODE = 'xx'
# Add to language catalog
LANGUAGES.append(('xx', 'Test'))
# Add custom languages not provided by Django
EXTRA_LANG_INFO = {
'xx': {
'code': 'xx',
'name': 'Test',
'name_local': 'Test'
},
}
LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
django.conf.locale.LANG_INFO = LANG_INFO
# Currencies available for use
CURRENCIES = CONFIG.get(
'currencies',

View File

@ -781,6 +781,7 @@ input[type="submit"] {
.btn-small {
padding: 3px;
padding-left: 5px;
padding-right: 5px;
}
.btn-remove {

View File

@ -106,7 +106,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
except NameError:
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
return
# Workers are not running: run it as synchronous task
_func(*args, **kwargs)

View File

@ -19,7 +19,7 @@ from base64 import b64encode
class HTMLAPITests(TestCase):
"""
Test that we can access the REST API endpoints via the HTML interface.
History: Discovered on 2021-06-28 a bug in InvenTreeModelSerializer,
which raised an AssertionError when using the HTML API interface,
while the regular JSON interface continued to work as expected.
@ -280,7 +280,7 @@ class APITests(InvenTreeAPITestCase):
"""
Tests for detail API endpoint actions
"""
self.basicAuth()
url = reverse('api-part-detail', kwargs={'pk': 1})

View File

@ -38,6 +38,7 @@ from rest_framework.documentation import include_docs_urls
from .views import auth_request
from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView
from .views import CustomSessionDeleteView, CustomSessionDeleteOtherView
from .views import CurrencyRefreshView
from .views import AppearanceSelectView, SettingCategorySelectView
from .views import DynamicJsView
@ -77,7 +78,7 @@ apipatterns = [
settings_urls = [
url(r'^i18n/?', include('django.conf.urls.i18n')),
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
@ -157,6 +158,10 @@ frontendpatterns = [
url(r'^search/', SearchView.as_view(), name='search'),
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
# DB user sessions
url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
url(r'^accounts/sessions/(?P<pk>\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ),
# Single Sign On / allauth
# overrides of urlpatterns
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),

View File

@ -12,11 +12,15 @@ import common.models
INVENTREE_SW_VERSION = "0.6.0 dev"
# InvenTree API version
INVENTREE_API_VERSION = 18
INVENTREE_API_VERSION = 19
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v19 -> 2021-12-02
- Adds the ability to filter the StockItem API by "part_tree"
- Returns only stock items which match a particular part.tree_id field
v18 -> 2021-11-15
- Adds the ability to filter BomItem API by "uses" field
- This returns a list of all BomItems which "use" the specified part
@ -120,10 +124,10 @@ def isInvenTreeDevelopmentVersion():
def inventreeDocsVersion():
"""
Return the version string matching the latest documentation.
Development -> "latest"
Release -> "major.minor.sub" e.g. "0.5.2"
"""
if isInvenTreeDevelopmentVersion():

View File

@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _
from django.template.loader import render_to_string
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.timezone import now
from django.shortcuts import redirect
from django.conf import settings
@ -29,6 +30,7 @@ from allauth.socialaccount.forms import DisconnectForm
from allauth.account.models import EmailAddress
from allauth.account.views import EmailView, PasswordResetFromKeyView
from allauth.socialaccount.views import ConnectionsView
from user_sessions.views import SessionDeleteView, SessionDeleteOtherView
from common.settings import currency_code_default, currency_codes
@ -733,6 +735,10 @@ class SettingsView(TemplateView):
ctx["request"] = self.request
ctx['social_form'] = DisconnectForm(request=self.request)
# user db sessions
ctx['session_key'] = self.request.session.session_key
ctx['session_list'] = self.request.user.session_set.filter(expire_date__gt=now()).order_by('-last_activity')
return ctx
@ -766,6 +772,20 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView):
success_url = reverse_lazy("account_login")
class UserSessionOverride():
"""overrides sucessurl to lead to settings"""
def get_success_url(self):
return str(reverse_lazy('settings'))
class CustomSessionDeleteView(UserSessionOverride, SessionDeleteView):
pass
class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView):
pass
class CurrencyRefreshView(RedirectView):
"""
POST endpoint to refresh / update exchange rates

View File

@ -198,7 +198,7 @@ class BuildUnallocate(generics.CreateAPIView):
queryset = Build.objects.none()
serializer_class = BuildUnallocationSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
@ -231,7 +231,7 @@ class BuildComplete(generics.CreateAPIView):
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
except:
pass
return ctx
@ -296,7 +296,7 @@ class BuildItemList(generics.ListCreateAPIView):
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
except AttributeError:
pass
return self.serializer_class(*args, **kwargs)
def get_queryset(self):

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0032_auto_20211014_0632'),
]
operations = [
migrations.AddField(
model_name='buildorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-12-01 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0033_auto_20211128_0151'),
]
operations = [
migrations.AlterField(
model_name='build',
name='reference_int',
field=models.BigIntegerField(default=0),
),
]

View File

@ -66,7 +66,7 @@ def get_next_build_number():
attempts.add(reference)
else:
break
return reference
@ -94,13 +94,13 @@ class Build(MPTTModel, ReferenceIndexingMixin):
"""
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
@staticmethod
def get_api_url():
return reverse('api-build-list')
def api_instance_filters(self):
return {
'parent': {
'exclude_tree': self.pk,
@ -1178,7 +1178,7 @@ class BuildItem(models.Model):
bom_item = PartModels.BomItem.objects.get(part=self.build.part, sub_part=ancestor)
except PartModels.BomItem.DoesNotExist:
continue
# A matching BOM item has been found!
if idx == 0 or bom_item.allow_variants:
bom_item_valid = True
@ -1234,7 +1234,7 @@ class BuildItem(models.Model):
thumb_url = self.stock_item.part.image.thumbnail.url
except:
pass
if thumb_url is None and self.bom_item and self.bom_item.sub_part:
try:
thumb_url = self.bom_item.sub_part.image.thumbnail.url

View File

@ -16,7 +16,7 @@ from rest_framework import serializers
from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin
import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField
@ -32,7 +32,7 @@ from users.serializers import OwnerSerializer
from .models import Build, BuildItem, BuildOrderAttachment
class BuildSerializer(InvenTreeModelSerializer):
class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
"""
Serializes a Build object
"""
@ -309,7 +309,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
)
def validate_bom_item(self, bom_item):
# TODO: Fix this validation - allow for variants and substitutes!
build = self.context['build']
@ -332,7 +332,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
if not stock_item.in_stock:
raise ValidationError(_("Item must be in stock"))
return stock_item
quantity = serializers.DecimalField(
@ -398,7 +398,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
# Output *cannot* be set for un-tracked parts
if output is not None and not bom_item.sub_part.trackable:
raise ValidationError({
'output': _('Build output cannot be specified for allocation of untracked parts')
})
@ -422,14 +422,14 @@ class BuildAllocationSerializer(serializers.Serializer):
"""
Validation
"""
super().validate(data)
items = data.get('items', [])
if len(items) == 0:
raise ValidationError(_('Allocation items must be provided'))
return data
def save(self):
@ -516,8 +516,6 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializer for a BuildAttachment
"""
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = BuildOrderAttachment
@ -525,6 +523,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
'pk',
'build',
'attachment',
'link',
'filename',
'comment',
'upload_date',

View File

@ -12,7 +12,7 @@
{% block breadcrumbs %}
<li class='breadcrumb-item'><a href='{% url "build-index" %}'>{% trans "Build Orders" %}</a></li>
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "build-detail" build.id %}'>{{ build }}</a></li>
{% endblock %}
{% endblock breadcrumbs %}
{% block thumbnail %}
<img class="part-thumb"
@ -21,7 +21,7 @@ src="{{ build.part.image.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
{% endblock %}
{% endblock thumbnail %}
{% block heading %}
{% trans "Build Order" %} {{ build }}
@ -66,11 +66,23 @@ src="{% static 'img/blank_image.png' %}"
</button>
{% endif %}
{% endif %}
{% endblock %}
{% endblock actions %}
{% block details %}
<p>{{ build.title }}</p>
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Part" %}</td>
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Build Description" %}</td>
<td>{{ build.title }}</td>
</tr>
</table>
<div class='info-messages'>
{% if build.sales_order %}
@ -114,11 +126,7 @@ src="{% static 'img/blank_image.png' %}"
{% block details_right %}
<table class='table table-striped table-condensed'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Part" %}</td>
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
</tr>
<col width='25'>
<tr>
<td></td>
<td>{% trans "Quantity" %}</td>

View File

@ -431,53 +431,17 @@ enableDragAndDrop(
}
);
// Callback for creating a new attachment
$('#new-attachment').click(function() {
constructForm('{% url "api-build-attachment-list" %}', {
fields: {
attachment: {},
comment: {},
build: {
value: {{ build.pk }},
hidden: true,
}
},
method: 'POST',
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Attachment" %}',
});
});
loadAttachmentTable(
'{% url "api-build-attachment-list" %}',
{
filters: {
build: {{ build.pk }},
},
onEdit: function(pk) {
var url = `/api/build/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/build/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
loadAttachmentTable('{% url "api-build-attachment-list" %}', {
filters: {
build: {{ build.pk }},
},
fields: {
build: {
value: {{ build.pk }},
hidden: true,
}
}
);
});
$('#edit-notes').click(function() {
constructForm('{% url "api-build-detail" build.pk %}', {

View File

@ -2,14 +2,21 @@
{% load static %}
{% load inventree_extras %}
{% include "sidebar_item.html" with label='details' text="Build Order Details" icon="fa-info-circle" %}
{% trans "Build Order Details" as text %}
{% include "sidebar_item.html" with label='details' text=text icon="fa-info-circle" %}
{% if build.active %}
{% include "sidebar_item.html" with label='allocate' text="Allocate Stock" icon="fa-tasks" %}
{% trans "Allocate Stock" as text %}
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %}
{% endif %}
{% if not build.is_complete %}
{% include "sidebar_item.html" with label='outputs' text="Pending Items" icon="fa-tools" %}
{% trans "Pending Items" as text %}
{% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %}
{% endif %}
{% include "sidebar_item.html" with label='completed' text="Completed Items" icon="fa-boxes" %}
{% include "sidebar_item.html" with label='children' text="Child Build Orders" icon="fa-sitemap" %}
{% include "sidebar_item.html" with label='attachments' text="Attachments" icon="fa-paperclip" %}
{% include "sidebar_item.html" with label='notes' text="Notes" icon="fa-clipboard" %}
{% trans "Completed Items" as text %}
{% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %}
{% trans "Child Build Orders" as text %}
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
{% trans "Attachments" as text %}
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label='notes' text=text icon="fa-clipboard" %}

View File

@ -73,7 +73,7 @@ class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
permission_classes = [
GlobalSettingsPermissions,
]
class UserSettingsList(SettingsList):
"""
@ -124,7 +124,7 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView):
queryset = common.models.InvenTreeUserSetting.objects.all()
serializer_class = common.serializers.UserSettingsSerializer
permission_classes = [
UserSettingsPermissions,
]

View File

@ -12,7 +12,7 @@ class CommonConfig(AppConfig):
name = 'common'
def ready(self):
self.clear_restart_flag()
def clear_restart_flag(self):
@ -22,7 +22,7 @@ class CommonConfig(AppConfig):
try:
import common.models
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'):
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)

View File

@ -108,7 +108,9 @@ class BaseInvenTreeSetting(models.Model):
for key, value in settings.items():
validator = cls.get_setting_validator(key)
if cls.validator_is_bool(validator):
if cls.is_protected(key):
value = '***'
elif cls.validator_is_bool(validator):
value = InvenTree.helpers.str2bool(value)
elif cls.validator_is_int(validator):
try:
@ -485,7 +487,7 @@ class BaseInvenTreeSetting(models.Model):
elif self.is_int():
return 'integer'
else:
return 'string'
@ -538,6 +540,19 @@ class BaseInvenTreeSetting(models.Model):
return value
@classmethod
def is_protected(cls, key):
"""
Check if the setting value is protected
"""
key = str(key).strip().upper()
if key in cls.GLOBAL_SETTINGS:
return cls.GLOBAL_SETTINGS[key].get('protected', False)
else:
return False
def settings_group_options():
"""build up group tuple for settings based on gour choices"""

View File

@ -45,6 +45,18 @@ class SettingsSerializer(InvenTreeModelSerializer):
return results
def get_value(self, obj):
"""
Make sure protected values are not returned
"""
result = obj.value
# never return protected values
if obj.is_protected:
result = '***'
return result
class GlobalSettingsSerializer(SettingsSerializer):
"""

View File

@ -170,7 +170,7 @@ class ManufacturerPartParameterList(generics.ListCreateAPIView):
queryset = ManufacturerPartParameter.objects.all()
serializer_class = ManufacturerPartParameterSerializer
def get_serializer(self, *args, **kwargs):
# Do we wish to include any extra detail?

View File

@ -477,7 +477,7 @@ class SupplierPart(models.Model):
return reverse('supplier-part-detail', kwargs={'pk': self.id})
def api_instance_filters(self):
return {
'manufacturer_part': {
'part': self.part.pk

View File

@ -187,7 +187,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
part_detail = kwargs.pop('part_detail', True)
supplier_detail = kwargs.pop('supplier_detail', True)
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
prettify = kwargs.pop('pretty', False)
super(SupplierPartSerializer, self).__init__(*args, **kwargs)

View File

@ -19,21 +19,26 @@
{% include "admin_button.html" with url=url %}
{% endif %}
{% if company.is_supplier and roles.purchase_order.add %}
<button type='button' class='btn btn-outline-secondary' id='company-order-2' title='{% trans "Create Purchase Order" %}'>
<button type='button' class='btn btn-outline-primary' id='company-order-2' title='{% trans "Create Purchase Order" %}'>
<span class='fas fa-shopping-cart'/>
</button>
{% endif %}
{% if perms.company.change_company %}
<button type='button' class='btn btn-outline-secondary' id='company-edit' title='{% trans "Edit company information" %}'>
<span class='fas fa-edit icon-green'/>
<button id='company-edit-actions' title='{% trans "Company actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
{% endif %}
{% if perms.company.delete_company %}
<button type='button' class='btn btn-outline-secondary' id='company-delete' title='{% trans "Delete Company" %}'>
<span class='fas fa-trash-alt icon-red'/>
</button>
{% endif %}
{% endblock %}
<ul class='dropdown-menu' role='menu'>
{% if perms.company.change_company %}
<li><a class='dropdown-item' href='#' id='company-edit' title='{% trans "Edit company information" %}'>
<span class='fas fa-edit icon-green'></span> {% trans "Edit Company" %}
</a></li>
{% endif %}
{% if perms.company.delete_company %}
<li><a class='dropdown-item' href='#' id='company-delete' title='{% trans "Delete company" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Company" %}
</a></li>
{% endif %}
</ul>
{% endblock actions %}
{% block thumbnail %}
<div class='dropzone part-thumb-container' id='company-thumb'>
@ -56,7 +61,29 @@
{% endblock %}
{% block details %}
<p>{{ company.description }}</p>
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Description" %}</td>
<td>{{ company.description }}</td>
</tr>
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{%trans "Manufacturer" %}</td>
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
</tr>
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
</tr>
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
</tr>
</table>
{% endblock %}
@ -110,22 +137,6 @@
<td>{{ company.contact }}{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{%trans "Manufacturer" %}</td>
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
</tr>
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
</tr>
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
</tr>
</table>
{% endblock %}

View File

@ -8,7 +8,7 @@ InvenTree | {% trans "Manufacturer Part" %}
{% block sidebar %}
{% include "company/manufacturer_part_sidebar.html" %}
{% endblock %}
{% endblock sidebar %}
{% block breadcrumbs %}
<li class='breadcrumb-item'><a href='{% url "manufacturer-index" %}'>{% trans "Manufacturers" %}</a></li>
@ -16,13 +16,13 @@ InvenTree | {% trans "Manufacturer Part" %}
<li class='breadcrumb-item'><a href='{% url "company-detail" part.manufacturer.id %}'>{{ part.manufacturer.name }}</a></li>
{% endif %}
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "manufacturer-part-detail" part.id %}'>{{ part.MPN }}</a></li>
{% endblock %}
{% endblock breadcrumbs %}
{% block heading %}
<h4>
{% trans "Manufacturer Part" %}: {{ part.part.full_name }}
</h4>
{% endblock %}
{% endblock heading %}
{% block actions %}
{% if user.is_staff and perms.company.change_company %}
@ -46,7 +46,7 @@ InvenTree | {% trans "Manufacturer Part" %}
</button>
{% endif %}
{% endif %}
{% endblock %}
{% endblock actions %}
{% block thumbnail %}
<img class='part-thumb'
@ -55,50 +55,56 @@ src='{{ part.part.image.url }}'
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
{% endblock %}
{% endblock thumbnail %}
{% block details %}
{% endblock %}
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Internal Part" %}</td>
<td>
{% if part.part %}
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
{% endif %}
</td>
</tr>
{% if part.description %}
<tr>
<td></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
{% endif %}
</table>
{% endblock details %}
{% block details_right %}
<table class="table table-striped table-condensed">
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Internal Part" %}</td>
<td>
{% if part.part %}
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
{% endif %}
</td>
</tr>
{% if part.description %}
<tr>
<td></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{% trans "Manufacturer" %}</td>
<td><a href="{% url 'company-detail' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td></tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "MPN" %}</td>
<td>{{ part.MPN }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{% trans "Manufacturer" %}</td>
<td><a href="{% url 'company-detail' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "MPN" %}</td>
<td>{{ part.MPN }}{% include "clip.html"%}</td>
</tr>
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
</table>
{% endblock %}
{% endblock details_right %}
{% block page_content %}

View File

@ -2,5 +2,7 @@
{% load static %}
{% load inventree_extras %}
{% include "sidebar_item.html" with label='parameters' text="Parameters" icon="fa-th-list" %}
{% include "sidebar_item.html" with label='supplier-parts' text="Supplier Parts" icon="fa-building" %}
{% trans "Parameters" as text %}
{% include "sidebar_item.html" with label='parameters' text=text icon="fa-th-list" %}
{% trans "Supplier Parts" as text %}
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}

View File

@ -3,17 +3,24 @@
{% load inventree_extras %}
{% if company.is_manufacturer %}
{% include "sidebar_item.html" with label='manufacturer-parts' text="Manufactured Parts" icon="fa-industry" %}
{% trans "Manufactured Parts" as text %}
{% include "sidebar_item.html" with label='manufacturer-parts' text=text icon="fa-industry" %}
{% endif %}
{% if company.is_supplier %}
{% include "sidebar_item.html" with label='supplier-parts' text="Supplied Parts" icon="fa-building" %}
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %}
{% trans "Supplied Parts" as text %}
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
{% trans "Purchase Orders" as text %}
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
{% endif %}
{% if company.is_manufacturer or company.is_supplier %}
{% include "sidebar_item.html" with label='company-stock' text="Supplied Stock Items" icon="fa-boxes" %}
{% trans "Supplied Stock Items" as text %}
{% include "sidebar_item.html" with label='company-stock' text=text icon="fa-boxes" %}
{% endif %}
{% if company.is_customer %}
{% include "sidebar_item.html" with label='sales-orders' text="Sales Orders" icon="fa-truck" %}
{% include "sidebar_item.html" with label='assigned-stock' text="Assigned Stock Items" icon="fa-sign-out-alt" %}
{% trans "Sales Orders" as text %}
{% include "sidebar_item.html" with label='sales-orders' text=text icon="fa-truck" %}
{% trans "Assigned Stock Items" as text %}
{% include "sidebar_item.html" with label='assigned-stock' text=text icon="fa-sign-out-alt" %}
{% endif %}
{% include "sidebar_item.html" with label='company-notes' text="Notes" icon="fa-clipboard" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label='company-notes' text=text icon="fa-clipboard" %}

View File

@ -5,11 +5,11 @@
{% block page_title %}
{% inventree_title %} | {% trans "Supplier Part" %}
{% endblock %}
{% endblock page_title %}
{% block sidebar %}
{% include "company/supplier_part_sidebar.html" %}
{% endblock %}
{% endblock sidebar %}
{% block breadcrumbs %}
<li class='breadcrumb-item'><a href='{% url "supplier-index" %}'>{% trans "Suppliers" %}</a></li>
@ -17,13 +17,13 @@
<li class='breadcrumb-item'><a href='{% url "company-detail" part.supplier.id %}'>{{ part.supplier.name }}</a></li>
{% endif %}
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "supplier-part-detail" part.id %}'>{{ part.SKU }}</a></li>
{% endblock %}
{% endblock breadcrumbs %}
{% block heading %}
<h4>
{% trans "Supplier Part" %}: {{ part.SKU }}
</h4>
{% endblock %}
{% endblock heading %}
{% block actions %}
{% if user.is_staff and perms.company.change_company %}
@ -43,7 +43,7 @@
<span class='fas fa-trash-alt icon-red'/>
</button>
{% endif %}
{% endblock %}
{% endblock actions %}
{% block thumbnail %}
<img class='part-thumb'
@ -56,39 +56,32 @@ src="{% static 'img/blank_image.png' %}"
{% block details %}
<p>
{{ part.part.full_name }}
</p>
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Internal Part" %}</td>
<td>
{% if part.part %}
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
{% endif %}
</td>
</tr>
{% if part.description %}
<tr>
<td></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
{% endif %}
</table>
{% endblock %}
{% endblock details %}
{% block details_right %}
<table class="table table-striped table-condensed">
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Internal Part" %}</td>
<td>
{% if part.part %}
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
{% endif %}
</td>
</tr>
{% if part.description %}
<tr>
<td></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td>
@ -127,6 +120,13 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ part.note }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
</table>
{% endblock %}

View File

@ -2,6 +2,9 @@
{% load static %}
{% load inventree_extras %}
{% include "sidebar_item.html" with label='stock' text="Stock Items" icon="fa-boxes" %}
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %}
{% include "sidebar_item.html" with label='pricing' text="Supplier Part Pricing" icon="fa-dollar-sign" %}
{% trans "Stock Items" as text %}
{% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %}
{% trans "Purchase Orders" as text %}
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
{% trans "Supplier Part Pricing" as text %}
{% include "sidebar_item.html" with label='pricing' text=text icon="fa-dollar-sign" %}

View File

@ -202,7 +202,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
data = {
'MPN': 'MPN-TEST-123',
}
response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -29,7 +29,7 @@ company_urls = [
]
manufacturer_part_urls = [
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
]

View File

@ -399,7 +399,7 @@ class PartLabelMixin:
if key in params:
parts = params.getlist(key, [])
break
valid_ids = []
for part in parts:

View File

@ -186,7 +186,7 @@ class LabelTemplate(models.Model):
"""
template_string = Template(self.filename_pattern)
ctx = self.context(request)
context = Context(ctx)

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@ class POList(generics.ListCreateAPIView):
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
item = serializer.save()
item.created_by = request.user
item.save()
@ -404,7 +404,7 @@ class SOList(generics.ListCreateAPIView):
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
item = serializer.save()
item.created_by = request.user
item.save()

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0052_auto_20211014_0631'),
]
operations = [
migrations.AddField(
model_name='purchaseorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AddField(
model_name='salesorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='purchaseorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
migrations.AlterField(
model_name='salesorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.5 on 2021-12-01 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0053_auto_20211128_0151'),
]
operations = [
migrations.AlterField(
model_name='purchaseorder',
name='reference_int',
field=models.BigIntegerField(default=0),
),
migrations.AlterField(
model_name='salesorder',
name='reference_int',
field=models.BigIntegerField(default=0),
),
]

View File

@ -772,7 +772,7 @@ class PurchaseOrderLineItem(OrderLineItem):
def get_base_part(self):
"""
Return the base part.Part object for the line item
Note: Returns None if the SupplierPart is not set!
"""
if self.part is None:

View File

@ -17,16 +17,16 @@ from rest_framework.serializers import ValidationError
from sql_util.utils import SubqueryCount
from common.settings import currency_code_mappings
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField
from InvenTree.serializers import ReferenceIndexingSerializerMixin
from InvenTree.status_codes import StockStatus
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from part.serializers import PartBriefSerializer
import stock.models
@ -37,10 +37,10 @@ from .models import PurchaseOrderAttachment, SalesOrderAttachment
from .models import SalesOrder, SalesOrderLineItem
from .models import SalesOrderAllocation
from common.settings import currency_code_mappings
from users.serializers import OwnerSerializer
class POSerializer(InvenTreeModelSerializer):
class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" Serializer for a PurchaseOrder object """
def __init__(self, *args, **kwargs):
@ -86,6 +86,8 @@ class POSerializer(InvenTreeModelSerializer):
reference = serializers.CharField(required=True)
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
class Meta:
model = PurchaseOrder
@ -100,6 +102,7 @@ class POSerializer(InvenTreeModelSerializer):
'overdue',
'reference',
'responsible',
'responsible_detail',
'supplier',
'supplier_detail',
'supplier_reference',
@ -374,8 +377,6 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializers for the PurchaseOrderAttachment model
"""
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = PurchaseOrderAttachment
@ -383,6 +384,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
'pk',
'order',
'attachment',
'link',
'filename',
'comment',
'upload_date',
@ -393,7 +395,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
]
class SalesOrderSerializer(InvenTreeModelSerializer):
class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
"""
Serializers for the SalesOrder object
"""
@ -553,10 +555,10 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
quantity = InvenTreeDecimalField()
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
sale_price = InvenTreeMoneySerializer(
allow_null=True
)
@ -594,8 +596,6 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializers for the SalesOrderAttachment model
"""
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = SalesOrderAttachment
@ -604,6 +604,7 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
'order',
'attachment',
'filename',
'link',
'comment',
'upload_date',
]

View File

@ -53,15 +53,17 @@
<span class='fas fa-shopping-cart icon-blue'></span>
</button>
{% elif order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-outline-secondary' id='receive-order' title='{% trans "Receive items" %}'>
<span class='fas fa-sign-in-alt icon-blue'></span>
<button type='button' class='btn btn-primary' id='receive-order' title='{% trans "Receive items" %}'>
<span class='fas fa-sign-in-alt'></span>
{% trans "Receive Items" %}
</button>
<button type='button' class='btn btn-outline-secondary' id='complete-order' title='{% trans "Mark order as complete" %}'>
<span class='fas fa-check-circle icon-green'></span>
<button type='button' class='btn btn-success' id='complete-order' title='{% trans "Mark order as complete" %}'>
<span class='fas fa-check-circle'></span>
{% trans "Complete Order" %}
</button>
{% endif %}
{% endif %}
{% endblock %}
{% endblock actions %}
{% block thumbnail %}
<img class='part-thumb'
@ -75,24 +77,18 @@ src="{% static 'img/blank_image.png' %}"
{% block details %}
<h4>
{% purchase_order_status_label order.status large=True %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}
</h4>
<p>{{ order.description }}{% include "clip.html"%}</p>
{% endblock %}
{% block details_right %}
<table class='table'>
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Order Description" %}</td>
<td>{{ order.description }}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td>
@ -103,6 +99,14 @@ src="{% static 'img/blank_image.png' %}"
{% endif %}
</td>
</tr>
</table>
{% endblock %}
{% block details_right %}
<table class='table table-condensed table-striped'>
<col width='25'>
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td>

View File

@ -5,7 +5,8 @@
{% block sidebar %}
{% url "po-detail" order.id as url %}
{% include "sidebar_item.html" with url=url text="Return to Orders" icon="fa-undo" %}
{% trans "Return to Orders" as text %}
{% include "sidebar_item.html" with url=url text=text icon="fa-undo" %}
{% endblock %}
{% block page_content %}

View File

@ -2,7 +2,11 @@
{% load static %}
{% load inventree_extras %}
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %}
{% include "sidebar_item.html" with label='received-items' text="Received Stock" icon="fa-sign-in-alt" %}
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %}
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %}
{% trans "Line Items" as text %}
{% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
{% trans "Received Stock" as text %}
{% include "sidebar_item.html" with label='received-items' text=text icon="fa-sign-in-alt" %}
{% trans "Attachments" as text %}
{% include "sidebar_item.html" with label='order-attachments' text=text icon="fa-paperclip" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label='order-notes' text=text icon="fa-clipboard" %}

View File

@ -27,7 +27,7 @@
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
</button>
{% elif order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-success' id='receive-selected-items' title='{% trans "Receive selected items" %}'>
<button type='button' class='btn btn-primary' id='receive-selected-items' title='{% trans "Receive selected items" %}'>
<span class='fas fa-sign-in-alt'></span> {% trans "Receive Items" %}
</button>
{% endif %}
@ -124,51 +124,16 @@
}
);
loadAttachmentTable(
'{% url "api-po-attachment-list" %}',
{
filters: {
order: {{ order.pk }},
},
onEdit: function(pk) {
var url = `/api/order/po/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/order/po/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
loadAttachmentTable('{% url "api-po-attachment-list" %}', {
filters: {
order: {{ order.pk }},
},
fields: {
order: {
value: {{ order.pk }},
hidden: true,
}
}
);
$("#new-attachment").click(function() {
constructForm('{% url "api-po-attachment-list" %}', {
method: 'POST',
fields: {
attachment: {},
comment: {},
order: {
value: {{ order.pk }},
hidden: true,
},
},
reload: true,
title: '{% trans "Add Attachment" %}',
});
});
loadStockTable($("#stock-table"), {

View File

@ -68,17 +68,33 @@ src="{% static 'img/blank_image.png' %}"
</button>
{% endif %}
{% endif %}
{% endblock %}
{% endblock actions %}
{% block details %}
<h4>
{% sales_order_status_label order.status large=True %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}
</h4>
<p>{{ order.description }}{% include "clip.html"%}</p>
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Order Description" %}</td>
<td>{{ order.description }}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td>
<td>
{% sales_order_status_label order.status %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}
</td>
</tr>
</table>
<div class='info-messages'>
{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
@ -93,21 +109,6 @@ src="{% static 'img/blank_image.png' %}"
{% block details_right %}
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td>
<td>
{% sales_order_status_label order.status %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}
</td>
</tr>
{% if order.customer %}
<tr>
<td><span class='fas fa-building'></span></td>

View File

@ -110,55 +110,21 @@
},
label: 'attachment',
success: function(data, status, xhr) {
location.reload();
reloadAttachmentTable();
}
}
);
loadAttachmentTable(
'{% url "api-so-attachment-list" %}',
{
filters: {
order: {{ order.pk }},
loadAttachmentTable('{% url "api-so-attachment-list" %}', {
filters: {
order: {{ order.pk }},
},
fields: {
order: {
value: {{ order.pk }},
hidden: true,
},
onEdit: function(pk) {
var url = `/api/order/so/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/order/so/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$("#new-attachment").click(function() {
constructForm('{% url "api-so-attachment-list" %}', {
method: 'POST',
fields: {
attachment: {},
comment: {},
order: {
value: {{ order.pk }},
hidden: true
}
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Attachment" %}'
});
});
loadBuildTable($("#builds-table"), {

View File

@ -2,7 +2,11 @@
{% load static %}
{% load inventree_extras %}
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %}
{% include "sidebar_item.html" with label='order-builds' text="Build Orders" icon="fa-tools" %}
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %}
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %}
{% trans "Line Items" as text %}
{% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
{% trans "Build Orders" as text %}
{% include "sidebar_item.html" with label='order-builds' text=text icon="fa-tools" %}
{% trans "Attachments" as text %}
{% include "sidebar_item.html" with label='order-attachments' text=text icon="fa-paperclip" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label='order-notes' text=text icon="fa-clipboard" %}

View File

@ -105,6 +105,25 @@ class PurchaseOrderTest(OrderTest):
self.assertEqual(data['pk'], 1)
self.assertEqual(data['description'], 'Ordering some screws')
def test_po_reference(self):
"""test that a reference with a too big / small reference is not possible"""
# get permissions
self.assignRole('purchase_order.add')
url = reverse('api-po-list')
huge_numer = 9223372036854775808
# too big
self.post(
url,
{
'supplier': 1,
'reference': huge_numer,
'description': 'PO not created via the API',
},
expected_code=400
)
def test_po_attachments(self):
url = reverse('api-po-attachment-list')
@ -228,7 +247,7 @@ class PurchaseOrderReceiveTest(OrderTest):
def setUp(self):
super().setUp()
self.assignRole('purchase_order.add')
self.url = reverse('api-po-receive', kwargs={'pk': 1})

View File

@ -406,7 +406,7 @@ class PurchaseOrderUpload(FileManagementFormView):
def done(self, form_list, **kwargs):
""" Once all the data is in, process it to add PurchaseOrderLineItem instances to the order """
order = self.get_order()
items = self.get_clean_items()
@ -432,7 +432,7 @@ class PurchaseOrderUpload(FileManagementFormView):
except IntegrityError:
# PurchaseOrderLineItem already exists
pass
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))
@ -449,7 +449,7 @@ class SalesOrderExport(AjaxView):
role_required = 'sales_order.view'
def get(self, request, *args, **kwargs):
order = get_object_or_404(SalesOrder, pk=self.kwargs.get('pk', None))
export_format = request.GET.get('format', 'csv')

View File

@ -205,7 +205,7 @@ class BomItemResource(ModelResource):
# If we are not generating an "import" template,
# just return the complete list of fields
if not self.is_importing:
if not getattr(self, 'is_importing', False):
return fields
# Otherwise, remove some fields we are not interested in

View File

@ -26,7 +26,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
from decimal import Decimal, InvalidOperation
from .models import Part, PartCategory
from .models import Part, PartCategory, PartRelated
from .models import BomItem, BomItemSubstitute
from .models import PartParameter, PartParameterTemplate
from .models import PartAttachment, PartTestTemplate
@ -42,7 +42,7 @@ from build.models import Build
from . import serializers as part_serializers
from InvenTree.helpers import str2bool, isNull
from InvenTree.helpers import str2bool, isNull, increment
from InvenTree.api import AttachmentMixin
from InvenTree.status_codes import BuildStatus
@ -169,7 +169,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for detail view of a single PartCategory object
"""
serializer_class = part_serializers.CategorySerializer
queryset = PartCategory.objects.all()
@ -222,7 +222,7 @@ class CategoryParameterList(generics.ListAPIView):
if category is not None:
try:
category = PartCategory.objects.get(pk=category)
fetch_parent = str2bool(params.get('fetch_parent', True))
@ -410,6 +410,33 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
]
class PartSerialNumberDetail(generics.RetrieveAPIView):
"""
API endpoint for returning extra serial number information about a particular part
"""
queryset = Part.objects.all()
def retrieve(self, request, *args, **kwargs):
part = self.get_object()
# Calculate the "latest" serial number
latest = part.getLatestSerialNumber()
data = {
'latest': latest,
}
if latest is not None:
next = increment(latest)
if next != increment:
data['next'] = next
return Response(data)
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single Part object """
@ -734,7 +761,7 @@ class PartList(generics.ListCreateAPIView):
raise ValidationError({
'initial_stock_quantity': [_('Must be a valid quantity')],
})
initial_stock_location = request.data.get('initial_stock_location', None)
try:
@ -850,7 +877,7 @@ class PartList(generics.ListCreateAPIView):
id_values.append(val)
except ValueError:
pass
queryset = queryset.exclude(pk__in=id_values)
# Exclude part variant tree?
@ -901,6 +928,40 @@ class PartList(generics.ListCreateAPIView):
queryset = queryset.filter(pk__in=pks)
# Filter by 'related' parts?
related = params.get('related', None)
exclude_related = params.get('exclude_related', None)
if related is not None or exclude_related is not None:
try:
pk = related if related is not None else exclude_related
pk = int(pk)
related_part = Part.objects.get(pk=pk)
part_ids = set()
# Return any relationship which points to the part in question
relation_filter = Q(part_1=related_part) | Q(part_2=related_part)
for relation in PartRelated.objects.filter(relation_filter):
if relation.part_1.pk != pk:
part_ids.add(relation.part_1.pk)
if relation.part_2.pk != pk:
part_ids.add(relation.part_2.pk)
if related is not None:
# Only return related results
queryset = queryset.filter(pk__in=[pk for pk in part_ids])
elif exclude_related is not None:
# Exclude related results
queryset = queryset.exclude(pk__in=[pk for pk in part_ids])
except (ValueError, Part.DoesNotExist):
pass
# Filter by 'starred' parts?
starred = params.get('starred', None)
@ -1014,9 +1075,48 @@ class PartList(generics.ListCreateAPIView):
'revision',
'keywords',
'category__name',
'manufacturer_parts__MPN',
]
class PartRelatedList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of PartRelated objects
"""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Add a filter for "part" - we can filter either part_1 or part_2
part = params.get('part', None)
if part is not None:
try:
part = Part.objects.get(pk=part)
queryset = queryset.filter(Q(part_1=part) | Q(part_2=part))
except (ValueError, Part.DoesNotExist):
pass
return queryset
class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for accessing detail view of a PartRelated object
"""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
class PartParameterTemplateList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameterTemplate objects.
@ -1081,24 +1181,6 @@ class BomFilter(rest_filters.FilterSet):
inherited = rest_filters.BooleanFilter(label='BOM line is inherited')
allow_variants = rest_filters.BooleanFilter(label='Variants are allowed')
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
def filter_validated(self, queryset, name, value):
# 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)
if str2bool(value):
queryset = queryset.filter(pk__in=pks)
else:
queryset = queryset.exclude(pk__in=pks)
return queryset
# Filters for linked 'part'
part_active = rest_filters.BooleanFilter(label='Master part is active', field_name='part__active')
part_trackable = rest_filters.BooleanFilter(label='Master part is trackable', field_name='part__trackable')
@ -1107,6 +1189,30 @@ class BomFilter(rest_filters.FilterSet):
sub_part_trackable = rest_filters.BooleanFilter(label='Sub part is trackable', field_name='sub_part__trackable')
sub_part_assembly = rest_filters.BooleanFilter(label='Sub part is an assembly', field_name='sub_part__assembly')
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
def filter_validated(self, queryset, name, value):
# Work out which lines have actually been validated
pks = []
value = str2bool(value)
# Shortcut for quicker filtering - BomItem with empty 'checksum' values are not validated
if value:
queryset = queryset.exclude(checksum=None).exclude(checksum='')
for bom_item in queryset.all():
if bom_item.is_line_valid:
pks.append(bom_item.pk)
if value:
queryset = queryset.filter(pk__in=pks)
else:
queryset = queryset.exclude(pk__in=pks)
return queryset
class BomList(generics.ListCreateAPIView):
"""
@ -1257,7 +1363,7 @@ class BomList(generics.ListCreateAPIView):
queryset = self.annotate_pricing(queryset)
return queryset
def include_pricing(self):
"""
Determine if pricing information should be included in the response
@ -1291,7 +1397,7 @@ class BomList(generics.ListCreateAPIView):
# Get default currency from settings
default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
if price:
if currency and default_currency:
try:
@ -1381,7 +1487,7 @@ class BomItemSubstituteList(generics.ListCreateAPIView):
serializer_class = part_serializers.BomItemSubstituteSerializer
queryset = BomItemSubstitute.objects.all()
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
@ -1435,6 +1541,12 @@ part_api_urls = [
url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'),
])),
# Base URL for PartRelated API endpoints
url(r'^related/', include([
url(r'^(?P<pk>\d+)/', PartRelatedDetail.as_view(), name='api-part-related-detail'),
url(r'^.*$', PartRelatedList.as_view(), name='api-part-related-list'),
])),
# Base URL for PartParameter API endpoints
url(r'^parameter/', include([
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
@ -1448,7 +1560,14 @@ part_api_urls = [
url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
])),
url(r'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'),
url(r'^(?P<pk>\d+)/', include([
# Endpoint for extra serial number information
url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'),
# Part detail endpoint
url(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
])),
url(r'^.*$', PartList.as_view(), name='api-part-list'),
]

View File

@ -59,7 +59,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
uids = []
def add_items(items, level, cascade):
def add_items(items, level, cascade=True):
# Add items at a given layer
for item in items:
@ -172,7 +172,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).prefetch_related('supplier_parts')
for mp_idx, mp_part in enumerate(manufacturer_parts):
# Extract the "name" field of the Manufacturer (Company)
@ -190,7 +190,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Generate a column name for this manufacturer
k_man = f'{_("Manufacturer")}_{mp_idx}'
k_mpn = f'{_("MPN")}_{mp_idx}'
try:
manufacturer_cols[k_man].update({bom_idx: manufacturer_name})
manufacturer_cols[k_mpn].update({bom_idx: manufacturer_mpn})
@ -200,7 +200,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# We wish to include supplier data for this manufacturer part
if supplier_data:
for sp_idx, sp_part in enumerate(mp_part.supplier_parts.all()):
supplier_parts_used.add(sp_part)

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