mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 04:26:44 +00:00
Merge branch 'master' into l10n_trans
This commit is contained in:
commit
37953f9926
5
.gitattributes
vendored
5
.gitattributes
vendored
@ -4,3 +4,8 @@
|
|||||||
*.md text
|
*.md text
|
||||||
*.html text
|
*.html text
|
||||||
*.txt text
|
*.txt text
|
||||||
|
*.yml text
|
||||||
|
*.yaml text
|
||||||
|
*.conf text
|
||||||
|
*.sh text
|
||||||
|
*.js text
|
3
.github/workflows/coverage.yaml
vendored
3
.github/workflows/coverage.yaml
vendored
@ -29,6 +29,7 @@ jobs:
|
|||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
sudo apt-get install gettext
|
||||||
pip3 install invoke
|
pip3 install invoke
|
||||||
invoke install
|
invoke install
|
||||||
- name: Coverage Tests
|
- name: Coverage Tests
|
||||||
@ -42,6 +43,8 @@ jobs:
|
|||||||
rm test_db.sqlite
|
rm test_db.sqlite
|
||||||
invoke migrate
|
invoke migrate
|
||||||
invoke import-records -f data.json
|
invoke import-records -f data.json
|
||||||
|
- name: Test Translations
|
||||||
|
run: invoke translate
|
||||||
- name: Check Migration Files
|
- name: Check Migration Files
|
||||||
run: python3 ci/check_migration_files.py
|
run: python3 ci/check_migration_files.py
|
||||||
- name: Upload Coverage Report
|
- name: Upload Coverage Report
|
||||||
|
31
.github/workflows/docker_build.yaml
vendored
31
.github/workflows/docker_build.yaml
vendored
@ -1,8 +1,11 @@
|
|||||||
# Test that the docker file builds correctly
|
# Build and push latest docker image on push to master branch
|
||||||
|
|
||||||
name: Docker
|
name: Docker Build
|
||||||
|
|
||||||
on: ["push", "pull_request"]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
@ -10,9 +13,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout Code
|
||||||
- name: Build Server Image
|
uses: actions/checkout@v2
|
||||||
run: cd docker/inventree && docker build . --tag inventree:$(date +%s)
|
- name: Login to Dockerhub
|
||||||
- name: Build nginx Image
|
uses: docker/login-action@v1
|
||||||
run: cd docker/nginx && docker build . --tag nxinx:$(date +%s)
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Build and Push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./docker
|
||||||
|
push: true
|
||||||
|
repository: inventree/inventree
|
||||||
|
tags: inventree/inventree:latest
|
||||||
|
- name: Image Digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
22
.github/workflows/docker_publish.yaml
vendored
22
.github/workflows/docker_publish.yaml
vendored
@ -7,12 +7,15 @@ on:
|
|||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
server_image:
|
publish_image:
|
||||||
name: Push InvenTree web server image to dockerhub
|
name: Push InvenTree web server image to dockerhub
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: cd
|
||||||
|
run: |
|
||||||
|
cd docker
|
||||||
- name: Push to Docker Hub
|
- name: Push to Docker Hub
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
@ -20,19 +23,4 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: inventree/inventree
|
repository: inventree/inventree
|
||||||
tag_with_ref: true
|
tag_with_ref: true
|
||||||
dockerfile: docker/inventree/Dockerfile
|
dockerfile: ./Dockerfile
|
||||||
|
|
||||||
nginx_image:
|
|
||||||
name: Push InvenTree nginx image to dockerhub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Push to Docker Hub
|
|
||||||
uses: docker/build-push-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
repository: inventree/nginx
|
|
||||||
tag_with_ref: true
|
|
||||||
dockerfile: docker/nginx/Dockerfile
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -40,6 +40,7 @@ docs/_build
|
|||||||
# Local static and media file storage (only when running in development mode)
|
# Local static and media file storage (only when running in development mode)
|
||||||
inventree_media
|
inventree_media
|
||||||
inventree_static
|
inventree_static
|
||||||
|
static_i18n
|
||||||
|
|
||||||
# Local config file
|
# Local config file
|
||||||
config.yaml
|
config.yaml
|
||||||
|
@ -280,11 +280,25 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs):
|
|||||||
json string of the supplied data plus some other data
|
json string of the supplied data plus some other data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
url = kwargs.get('url', False)
|
||||||
brief = kwargs.get('brief', True)
|
brief = kwargs.get('brief', True)
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
if brief:
|
if url:
|
||||||
|
request = object_data.get('request', None)
|
||||||
|
item_url = object_data.get('item_url', None)
|
||||||
|
absolute_url = None
|
||||||
|
|
||||||
|
if request and item_url:
|
||||||
|
absolute_url = request.build_absolute_uri(item_url)
|
||||||
|
# Return URL (No JSON)
|
||||||
|
return absolute_url
|
||||||
|
|
||||||
|
if item_url:
|
||||||
|
# Return URL (No JSON)
|
||||||
|
return item_url
|
||||||
|
elif brief:
|
||||||
data[object_name] = object_pk
|
data[object_name] = object_pk
|
||||||
else:
|
else:
|
||||||
data['tool'] = 'InvenTree'
|
data['tool'] = 'InvenTree'
|
||||||
|
61
InvenTree/InvenTree/management/commands/prerender.py
Normal file
61
InvenTree/InvenTree/management/commands/prerender.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Custom management command to prerender files
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from django.utils.translation import override as lang_over
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def render_file(file_name, source, target, locales, ctx):
|
||||||
|
""" renders a file into all provided locales """
|
||||||
|
for locale in locales:
|
||||||
|
target_file = os.path.join(target, locale + '.' + file_name)
|
||||||
|
with open(target_file, 'w') as localised_file:
|
||||||
|
with lang_over(locale):
|
||||||
|
renderd = render_to_string(os.path.join(source, file_name), ctx)
|
||||||
|
localised_file.write(renderd)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""
|
||||||
|
django command to prerender files
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
# static directories
|
||||||
|
LC_DIR = settings.LOCALE_PATHS[0]
|
||||||
|
SOURCE_DIR = settings.STATICFILES_I18_SRC
|
||||||
|
TARGET_DIR = settings.STATICFILES_I18_TRG
|
||||||
|
|
||||||
|
# ensure static directory exists
|
||||||
|
if not os.path.exists(TARGET_DIR):
|
||||||
|
os.makedirs(TARGET_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# collect locales
|
||||||
|
locales = {}
|
||||||
|
for locale in os.listdir(LC_DIR):
|
||||||
|
path = os.path.join(LC_DIR, locale)
|
||||||
|
if os.path.exists(path) and os.path.isdir(path):
|
||||||
|
locales[locale] = locale
|
||||||
|
|
||||||
|
# render!
|
||||||
|
request = HttpRequest()
|
||||||
|
ctx = {}
|
||||||
|
processors = tuple(import_string(path) for path in settings.STATFILES_I18_PROCESSORS)
|
||||||
|
for processor in processors:
|
||||||
|
ctx.update(processor(request))
|
||||||
|
|
||||||
|
for file in os.listdir(SOURCE_DIR, ):
|
||||||
|
path = os.path.join(SOURCE_DIR, file)
|
||||||
|
if os.path.exists(path) and os.path.isfile(path):
|
||||||
|
print(f"render {file}")
|
||||||
|
render_file(file, SOURCE_DIR, TARGET_DIR, locales, ctx)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Using multi-level directories is not implemented at this point') # TODO multilevel dir if needed
|
||||||
|
print(f"rendered all files in {SOURCE_DIR}")
|
@ -52,6 +52,9 @@ def get_setting(environment_var, backup_val, default_value=None):
|
|||||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||||
TESTING = 'test' in sys.argv
|
TESTING = 'test' in sys.argv
|
||||||
|
|
||||||
|
# New requirement for django 3.2+
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@ -190,6 +193,17 @@ STATICFILES_DIRS = [
|
|||||||
os.path.join(BASE_DIR, 'InvenTree', 'static'),
|
os.path.join(BASE_DIR, 'InvenTree', 'static'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Translated Template settings
|
||||||
|
STATICFILES_I18_PREFIX = 'i18n'
|
||||||
|
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js')
|
||||||
|
STATICFILES_I18_TRG = STATICFILES_DIRS[0] + '_' + STATICFILES_I18_PREFIX
|
||||||
|
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
|
||||||
|
STATICFILES_I18_TRG = os.path.join(STATICFILES_I18_TRG, STATICFILES_I18_PREFIX)
|
||||||
|
|
||||||
|
STATFILES_I18_PROCESSORS = [
|
||||||
|
'InvenTree.context.status_codes',
|
||||||
|
]
|
||||||
|
|
||||||
# Color Themes Directory
|
# Color Themes Directory
|
||||||
STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
|
STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
|
||||||
|
|
||||||
@ -396,7 +410,6 @@ for key in db_keys:
|
|||||||
env_var = os.environ.get(env_key, None)
|
env_var = os.environ.get(env_key, None)
|
||||||
|
|
||||||
if env_var:
|
if env_var:
|
||||||
logger.info(f"{env_key}={env_var}")
|
|
||||||
# Override configuration value
|
# Override configuration value
|
||||||
db_config[key] = env_var
|
db_config[key] = env_var
|
||||||
|
|
||||||
|
@ -185,6 +185,10 @@
|
|||||||
color: #c55;
|
color: #c55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-orange {
|
||||||
|
color: #fcba03;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-green {
|
.icon-green {
|
||||||
color: #43bb43;
|
color: #43bb43;
|
||||||
}
|
}
|
||||||
|
@ -56,17 +56,26 @@ def is_email_configured():
|
|||||||
configured = True
|
configured = True
|
||||||
|
|
||||||
if not settings.EMAIL_HOST:
|
if not settings.EMAIL_HOST:
|
||||||
logger.warning("EMAIL_HOST is not configured")
|
|
||||||
configured = False
|
configured = False
|
||||||
|
|
||||||
|
# Display warning unless in test mode
|
||||||
|
if not settings.TESTING:
|
||||||
|
logger.warning("EMAIL_HOST is not configured")
|
||||||
|
|
||||||
if not settings.EMAIL_HOST_USER:
|
if not settings.EMAIL_HOST_USER:
|
||||||
logger.warning("EMAIL_HOST_USER is not configured")
|
|
||||||
configured = False
|
configured = False
|
||||||
|
|
||||||
|
# Display warning unless in test mode
|
||||||
|
if not settings.TESTING:
|
||||||
|
logger.warning("EMAIL_HOST_USER is not configured")
|
||||||
|
|
||||||
if not settings.EMAIL_HOST_PASSWORD:
|
if not settings.EMAIL_HOST_PASSWORD:
|
||||||
logger.warning("EMAIL_HOST_PASSWORD is not configured")
|
|
||||||
configured = False
|
configured = False
|
||||||
|
|
||||||
|
# Display warning unless in test mode
|
||||||
|
if not settings.TESTING:
|
||||||
|
logger.warning("EMAIL_HOST_PASSWORD is not configured")
|
||||||
|
|
||||||
return configured
|
return configured
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +62,14 @@ class StatusCode:
|
|||||||
def items(cls):
|
def items(cls):
|
||||||
return cls.options.items()
|
return cls.options.items()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def keys(cls):
|
||||||
|
return cls.options.keys()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def labels(cls):
|
||||||
|
return cls.options.values()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def label(cls, value):
|
def label(cls, value):
|
||||||
""" Return the status code label associated with the provided value """
|
""" Return the status code label associated with the provided value """
|
||||||
|
@ -11,6 +11,7 @@ from django.contrib import admin
|
|||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
|
|
||||||
from company.urls import company_urls
|
from company.urls import company_urls
|
||||||
|
from company.urls import manufacturer_part_urls
|
||||||
from company.urls import supplier_part_urls
|
from company.urls import supplier_part_urls
|
||||||
from company.urls import price_break_urls
|
from company.urls import price_break_urls
|
||||||
|
|
||||||
@ -115,6 +116,7 @@ dynamic_javascript_urls = [
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^part/', include(part_urls)),
|
url(r'^part/', include(part_urls)),
|
||||||
|
url(r'^manufacturer-part/', include(manufacturer_part_urls)),
|
||||||
url(r'^supplier-part/', include(supplier_part_urls)),
|
url(r'^supplier-part/', include(supplier_part_urls)),
|
||||||
url(r'^price-break/', include(price_break_urls)),
|
url(r'^price-break/', include(price_break_urls)),
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import re
|
|||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.2.1 pre"
|
INVENTREE_SW_VERSION = "0.2.2 pre"
|
||||||
|
|
||||||
# Increment this number whenever there is a significant change to the API that any clients need to know about
|
# Increment this number whenever there is a significant change to the API that any clients need to know about
|
||||||
INVENTREE_API_VERSION = 2
|
INVENTREE_API_VERSION = 2
|
||||||
@ -19,6 +19,14 @@ def inventreeInstanceName():
|
|||||||
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
|
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
|
||||||
|
|
||||||
|
|
||||||
|
def inventreeInstanceTitle():
|
||||||
|
""" Returns the InstanceTitle for the current database """
|
||||||
|
if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False):
|
||||||
|
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
|
||||||
|
else:
|
||||||
|
return 'InvenTree'
|
||||||
|
|
||||||
|
|
||||||
def inventreeVersion():
|
def inventreeVersion():
|
||||||
""" Returns the InvenTree version string """
|
""" Returns the InvenTree version string """
|
||||||
return INVENTREE_SW_VERSION
|
return INVENTREE_SW_VERSION
|
||||||
|
@ -11,7 +11,7 @@ from rest_framework import generics
|
|||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool, isNull
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
|
|
||||||
from .models import Build, BuildItem
|
from .models import Build, BuildItem
|
||||||
@ -194,7 +194,11 @@ class BuildItemList(generics.ListCreateAPIView):
|
|||||||
output = params.get('output', None)
|
output = params.get('output', None)
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
queryset = queryset.filter(install_into=output)
|
|
||||||
|
if isNull(output):
|
||||||
|
queryset = queryset.filter(install_into=None)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(install_into=output)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ Django Forms for interacting with Build objects
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@ -12,6 +13,8 @@ from InvenTree.forms import HelperForm
|
|||||||
from InvenTree.fields import RoundingDecimalFormField
|
from InvenTree.fields import RoundingDecimalFormField
|
||||||
from InvenTree.fields import DatePickerFormField
|
from InvenTree.fields import DatePickerFormField
|
||||||
|
|
||||||
|
from InvenTree.status_codes import StockStatus
|
||||||
|
|
||||||
from .models import Build, BuildItem, BuildOrderAttachment
|
from .models import Build, BuildItem, BuildOrderAttachment
|
||||||
|
|
||||||
from stock.models import StockLocation, StockItem
|
from stock.models import StockLocation, StockItem
|
||||||
@ -165,16 +168,10 @@ class AutoAllocateForm(HelperForm):
|
|||||||
|
|
||||||
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm stock allocation'))
|
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm stock allocation'))
|
||||||
|
|
||||||
# Keep track of which build output we are interested in
|
|
||||||
output = forms.ModelChoiceField(
|
|
||||||
queryset=StockItem.objects.all(),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Build
|
model = Build
|
||||||
fields = [
|
fields = [
|
||||||
'confirm',
|
'confirm',
|
||||||
'output',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -214,6 +211,13 @@ class CompleteBuildOutputForm(HelperForm):
|
|||||||
help_text=_('Location of completed parts'),
|
help_text=_('Location of completed parts'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stock_status = forms.ChoiceField(
|
||||||
|
label=_('Status'),
|
||||||
|
help_text=_('Build output stock status'),
|
||||||
|
initial=StockStatus.OK,
|
||||||
|
choices=StockStatus.items(),
|
||||||
|
)
|
||||||
|
|
||||||
confirm_incomplete = forms.BooleanField(
|
confirm_incomplete = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Confirm incomplete'),
|
label=_('Confirm incomplete'),
|
||||||
@ -232,10 +236,15 @@ class CompleteBuildOutputForm(HelperForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'location',
|
'location',
|
||||||
'output',
|
'output',
|
||||||
|
'stock_status',
|
||||||
'confirm',
|
'confirm',
|
||||||
'confirm_incomplete',
|
'confirm_incomplete',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CancelBuildForm(HelperForm):
|
class CancelBuildForm(HelperForm):
|
||||||
""" Form for cancelling a build """
|
""" Form for cancelling a build """
|
||||||
|
@ -22,7 +22,7 @@ from markdownx.models import MarkdownxField
|
|||||||
|
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||||
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
||||||
from InvenTree.validators import validate_build_order_reference
|
from InvenTree.validators import validate_build_order_reference
|
||||||
from InvenTree.models import InvenTreeAttachment
|
from InvenTree.models import InvenTreeAttachment
|
||||||
@ -314,6 +314,42 @@ class Build(MPTTModel):
|
|||||||
'sub_part'
|
'sub_part'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tracked_bom_items(self):
|
||||||
|
"""
|
||||||
|
Returns the "trackable" BOM items for this BuildOrder
|
||||||
|
"""
|
||||||
|
|
||||||
|
items = self.bom_items
|
||||||
|
items = items.filter(sub_part__trackable=True)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def has_tracked_bom_items(self):
|
||||||
|
"""
|
||||||
|
Returns True if this BuildOrder has trackable BomItems
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.tracked_bom_items.count() > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def untracked_bom_items(self):
|
||||||
|
"""
|
||||||
|
Returns the "non trackable" BOM items for this BuildOrder
|
||||||
|
"""
|
||||||
|
|
||||||
|
items = self.bom_items
|
||||||
|
items = items.filter(sub_part__trackable=False)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def has_untracked_bom_items(self):
|
||||||
|
"""
|
||||||
|
Returns True if this BuildOrder has non trackable BomItems
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.untracked_bom_items.count() > 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remaining(self):
|
def remaining(self):
|
||||||
"""
|
"""
|
||||||
@ -449,6 +485,9 @@ class Build(MPTTModel):
|
|||||||
if self.completed < self.quantity:
|
if self.completed < self.quantity:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not self.areUntrackedPartsFullyAllocated():
|
||||||
|
return False
|
||||||
|
|
||||||
# No issues!
|
# No issues!
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -458,7 +497,7 @@ class Build(MPTTModel):
|
|||||||
Mark this build as complete
|
Mark this build as complete
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.can_complete:
|
if self.incomplete_count > 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.completion_date = datetime.now().date()
|
self.completion_date = datetime.now().date()
|
||||||
@ -466,6 +505,9 @@ class Build(MPTTModel):
|
|||||||
self.status = BuildStatus.COMPLETE
|
self.status = BuildStatus.COMPLETE
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
# Remove untracked allocated stock
|
||||||
|
self.subtractUntrackedStock(user)
|
||||||
|
|
||||||
# Ensure that there are no longer any BuildItem objects
|
# Ensure that there are no longer any BuildItem objects
|
||||||
# which point to thie Build Order
|
# which point to thie Build Order
|
||||||
self.allocated_stock.all().delete()
|
self.allocated_stock.all().delete()
|
||||||
@ -489,7 +531,7 @@ class Build(MPTTModel):
|
|||||||
self.status = BuildStatus.CANCELLED
|
self.status = BuildStatus.CANCELLED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def getAutoAllocations(self, output):
|
def getAutoAllocations(self):
|
||||||
"""
|
"""
|
||||||
Return a list of StockItem objects which will be allocated
|
Return a list of StockItem objects which will be allocated
|
||||||
using the 'AutoAllocate' function.
|
using the 'AutoAllocate' function.
|
||||||
@ -521,15 +563,19 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
part = bom_item.sub_part
|
part = bom_item.sub_part
|
||||||
|
|
||||||
|
# If the part is "trackable" it cannot be auto-allocated
|
||||||
|
if part.trackable:
|
||||||
|
continue
|
||||||
|
|
||||||
# Skip any parts which are already fully allocated
|
# Skip any parts which are already fully allocated
|
||||||
if self.isPartFullyAllocated(part, output):
|
if self.isPartFullyAllocated(part, None):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# How many parts are required to complete the output?
|
# How many parts are required to complete the output?
|
||||||
required = self.unallocatedQuantity(part, output)
|
required = self.unallocatedQuantity(part, None)
|
||||||
|
|
||||||
# Grab a list of stock items which are available
|
# Grab a list of stock items which are available
|
||||||
stock_items = self.availableStockItems(part, output)
|
stock_items = self.availableStockItems(part, None)
|
||||||
|
|
||||||
# Ensure that the available stock items are in the correct location
|
# Ensure that the available stock items are in the correct location
|
||||||
if self.take_from is not None:
|
if self.take_from is not None:
|
||||||
@ -544,7 +590,6 @@ class Build(MPTTModel):
|
|||||||
build_items = BuildItem.objects.filter(
|
build_items = BuildItem.objects.filter(
|
||||||
build=self,
|
build=self,
|
||||||
stock_item=stock_item,
|
stock_item=stock_item,
|
||||||
install_into=output
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(build_items) > 0:
|
if len(build_items) > 0:
|
||||||
@ -567,24 +612,45 @@ class Build(MPTTModel):
|
|||||||
return allocations
|
return allocations
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def unallocateStock(self, output=None, part=None):
|
def unallocateOutput(self, output, part=None):
|
||||||
"""
|
"""
|
||||||
Deletes all stock allocations for this build.
|
Unallocate all stock which are allocated against the provided "output" (StockItem)
|
||||||
|
|
||||||
Args:
|
|
||||||
output: Specify which build output to delete allocations (optional)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allocations = BuildItem.objects.filter(build=self.pk)
|
allocations = BuildItem.objects.filter(
|
||||||
|
build=self,
|
||||||
if output:
|
install_into=output
|
||||||
allocations = allocations.filter(install_into=output.pk)
|
)
|
||||||
|
|
||||||
if part:
|
if part:
|
||||||
allocations = allocations.filter(stock_item__part=part)
|
allocations = allocations.filter(stock_item__part=part)
|
||||||
|
|
||||||
# Remove all the allocations
|
allocations.delete()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def unallocateUntracked(self, part=None):
|
||||||
|
"""
|
||||||
|
Unallocate all "untracked" stock
|
||||||
|
"""
|
||||||
|
|
||||||
|
allocations = BuildItem.objects.filter(
|
||||||
|
build=self,
|
||||||
|
install_into=None
|
||||||
|
)
|
||||||
|
|
||||||
|
if part:
|
||||||
|
allocations = allocations.filter(stock_item__part=part)
|
||||||
|
|
||||||
|
allocations.delete()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def unallocateAll(self):
|
||||||
|
"""
|
||||||
|
Deletes all stock allocations for this build.
|
||||||
|
"""
|
||||||
|
|
||||||
|
allocations = BuildItem.objects.filter(build=self)
|
||||||
|
|
||||||
allocations.delete()
|
allocations.delete()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@ -679,13 +745,13 @@ class Build(MPTTModel):
|
|||||||
raise ValidationError(_("Build output does not match Build Order"))
|
raise ValidationError(_("Build output does not match Build Order"))
|
||||||
|
|
||||||
# Unallocate all build items against the output
|
# Unallocate all build items against the output
|
||||||
self.unallocateStock(output)
|
self.unallocateOutput(output)
|
||||||
|
|
||||||
# Remove the build output from the database
|
# Remove the build output from the database
|
||||||
output.delete()
|
output.delete()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def autoAllocate(self, output):
|
def autoAllocate(self):
|
||||||
"""
|
"""
|
||||||
Run auto-allocation routine to allocate StockItems to this Build.
|
Run auto-allocation routine to allocate StockItems to this Build.
|
||||||
|
|
||||||
@ -702,7 +768,7 @@ class Build(MPTTModel):
|
|||||||
See: getAutoAllocations()
|
See: getAutoAllocations()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allocations = self.getAutoAllocations(output)
|
allocations = self.getAutoAllocations()
|
||||||
|
|
||||||
for item in allocations:
|
for item in allocations:
|
||||||
# Create a new allocation
|
# Create a new allocation
|
||||||
@ -710,11 +776,29 @@ class Build(MPTTModel):
|
|||||||
build=self,
|
build=self,
|
||||||
stock_item=item['stock_item'],
|
stock_item=item['stock_item'],
|
||||||
quantity=item['quantity'],
|
quantity=item['quantity'],
|
||||||
install_into=output,
|
install_into=None
|
||||||
)
|
)
|
||||||
|
|
||||||
build_item.save()
|
build_item.save()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def subtractUntrackedStock(self, user):
|
||||||
|
"""
|
||||||
|
Called when the Build is marked as "complete",
|
||||||
|
this function removes the allocated untracked items from stock.
|
||||||
|
"""
|
||||||
|
|
||||||
|
items = self.allocated_stock.filter(
|
||||||
|
stock_item__part__trackable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove stock
|
||||||
|
for item in items:
|
||||||
|
item.complete_allocation(user)
|
||||||
|
|
||||||
|
# Delete allocation
|
||||||
|
items.all().delete()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def completeBuildOutput(self, output, user, **kwargs):
|
def completeBuildOutput(self, output, user, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -726,6 +810,7 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
# Select the location for the build output
|
# Select the location for the build output
|
||||||
location = kwargs.get('location', self.destination)
|
location = kwargs.get('location', self.destination)
|
||||||
|
status = kwargs.get('status', StockStatus.OK)
|
||||||
|
|
||||||
# List the allocated BuildItem objects for the given output
|
# List the allocated BuildItem objects for the given output
|
||||||
allocated_items = output.items_to_install.all()
|
allocated_items = output.items_to_install.all()
|
||||||
@ -733,9 +818,7 @@ class Build(MPTTModel):
|
|||||||
for build_item in allocated_items:
|
for build_item in allocated_items:
|
||||||
|
|
||||||
# TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
|
# TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
|
||||||
# TODO: Use celery / redis to offload the actual object deletion...
|
# TODO: Use the background worker process to handle this task!
|
||||||
# REF: https://www.botreetechnologies.com/blog/implementing-celery-using-django-for-background-task-processing
|
|
||||||
# REF: https://code.tutsplus.com/tutorials/using-celery-with-django-for-background-task-processing--cms-28732
|
|
||||||
|
|
||||||
# Complete the allocation of stock for that item
|
# Complete the allocation of stock for that item
|
||||||
build_item.complete_allocation(user)
|
build_item.complete_allocation(user)
|
||||||
@ -747,6 +830,7 @@ class Build(MPTTModel):
|
|||||||
output.build = self
|
output.build = self
|
||||||
output.is_building = False
|
output.is_building = False
|
||||||
output.location = location
|
output.location = location
|
||||||
|
output.status = status
|
||||||
|
|
||||||
output.save()
|
output.save()
|
||||||
|
|
||||||
@ -779,7 +863,7 @@ class Build(MPTTModel):
|
|||||||
if output:
|
if output:
|
||||||
quantity *= output.quantity
|
quantity *= output.quantity
|
||||||
else:
|
else:
|
||||||
quantity *= self.remaining
|
quantity *= self.quantity
|
||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
@ -807,7 +891,13 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
allocations = self.allocatedItems(part, output)
|
allocations = self.allocatedItems(part, output)
|
||||||
|
|
||||||
allocated = allocations.aggregate(q=Coalesce(Sum('quantity'), 0))
|
allocated = allocations.aggregate(
|
||||||
|
q=Coalesce(
|
||||||
|
Sum('quantity'),
|
||||||
|
0,
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return allocated['q']
|
return allocated['q']
|
||||||
|
|
||||||
@ -828,19 +918,39 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
return self.unallocatedQuantity(part, output) == 0
|
return self.unallocatedQuantity(part, output) == 0
|
||||||
|
|
||||||
def isFullyAllocated(self, output):
|
def isFullyAllocated(self, output, verbose=False):
|
||||||
"""
|
"""
|
||||||
Returns True if the particular build output is fully allocated.
|
Returns True if the particular build output is fully allocated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for bom_item in self.bom_items:
|
# If output is not specified, we are talking about "untracked" items
|
||||||
|
if output is None:
|
||||||
|
bom_items = self.untracked_bom_items
|
||||||
|
else:
|
||||||
|
bom_items = self.tracked_bom_items
|
||||||
|
|
||||||
|
fully_allocated = True
|
||||||
|
|
||||||
|
for bom_item in bom_items:
|
||||||
part = bom_item.sub_part
|
part = bom_item.sub_part
|
||||||
|
|
||||||
if not self.isPartFullyAllocated(part, output):
|
if not self.isPartFullyAllocated(part, output):
|
||||||
return False
|
fully_allocated = False
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"Part {part} is not fully allocated for output {output}")
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
# All parts must be fully allocated!
|
# All parts must be fully allocated!
|
||||||
return True
|
return fully_allocated
|
||||||
|
|
||||||
|
def areUntrackedPartsFullyAllocated(self):
|
||||||
|
"""
|
||||||
|
Returns True if the un-tracked parts are fully allocated for this BuildOrder
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.isFullyAllocated(None)
|
||||||
|
|
||||||
def allocatedParts(self, output):
|
def allocatedParts(self, output):
|
||||||
"""
|
"""
|
||||||
@ -849,7 +959,13 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
allocated = []
|
allocated = []
|
||||||
|
|
||||||
for bom_item in self.bom_items:
|
# If output is not specified, we are talking about "untracked" items
|
||||||
|
if output is None:
|
||||||
|
bom_items = self.untracked_bom_items
|
||||||
|
else:
|
||||||
|
bom_items = self.tracked_bom_items
|
||||||
|
|
||||||
|
for bom_item in bom_items:
|
||||||
part = bom_item.sub_part
|
part = bom_item.sub_part
|
||||||
|
|
||||||
if self.isPartFullyAllocated(part, output):
|
if self.isPartFullyAllocated(part, output):
|
||||||
@ -864,7 +980,13 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
unallocated = []
|
unallocated = []
|
||||||
|
|
||||||
for bom_item in self.bom_items:
|
# If output is not specified, we are talking about "untracked" items
|
||||||
|
if output is None:
|
||||||
|
bom_items = self.untracked_bom_items
|
||||||
|
else:
|
||||||
|
bom_items = self.tracked_bom_items
|
||||||
|
|
||||||
|
for bom_item in bom_items:
|
||||||
part = bom_item.sub_part
|
part = bom_item.sub_part
|
||||||
|
|
||||||
if not self.isPartFullyAllocated(part, output):
|
if not self.isPartFullyAllocated(part, output):
|
||||||
@ -1014,10 +1136,12 @@ class BuildItem(models.Model):
|
|||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
if not self.install_into:
|
|
||||||
raise ValidationError(_('Build item must specify a build output'))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
# If the 'part' is trackable, then the 'install_into' field must be set!
|
||||||
|
if self.stock_item.part and self.stock_item.part.trackable and not self.install_into:
|
||||||
|
raise ValidationError(_('Build item must specify a build output, as master part is marked as trackable'))
|
||||||
|
|
||||||
# Allocated part must be in the BOM for the master part
|
# Allocated part must be in the BOM for the master part
|
||||||
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
|
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
|
||||||
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
|
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | Allocate Parts
|
{% inventree_title %} | {% trans "Allocate Parts" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block menubar %}
|
{% block menubar %}
|
||||||
@ -12,48 +12,41 @@ InvenTree | Allocate Parts
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Incomplete Build Ouputs" %}
|
{% trans "Allocate Stock to Build" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
{% if build.is_complete %}
|
{% if build.has_untracked_bom_items %}
|
||||||
<div class='alert alert-block alert-success'>
|
{% if build.active %}
|
||||||
{% trans "Build order has been completed" %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
{% if build.active %}
|
<button class='btn btn-success' type='button' id='btn-auto-allocate' title='{% trans "Allocate stock to build" %}'>
|
||||||
<button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
|
<span class='fas fa-magic'></span> {% trans "Auto Allocate" %}
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "Create New Output" %}
|
|
||||||
</button>
|
</button>
|
||||||
<!--
|
|
||||||
<button class='btn btn-primary' type='button' id='btn-order-parts' title='{% trans "Order required parts" %}'>
|
|
||||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
|
|
||||||
</button>
|
|
||||||
-->
|
|
||||||
<button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Unallocate stock" %}'>
|
<button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Unallocate stock" %}'>
|
||||||
<span class='fas fa-minus-circle'></span> {% trans "Unallocate Stock" %}
|
<span class='fas fa-minus-circle'></span> {% trans "Unallocate Stock" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
<!--
|
||||||
|
<button class='btn btn-primary' type='button' id='btn-order-parts' title='{% trans "Order required parts" %}'>
|
||||||
|
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
|
||||||
|
</button>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
|
{% if build.areUntrackedPartsFullyAllocated %}
|
||||||
<hr>
|
<div class='alert alert-block alert-success'>
|
||||||
{% if build.incomplete_outputs %}
|
{% trans "Untracked stock has been fully allocated for this Build Order" %}
|
||||||
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
|
||||||
{% for item in build.incomplete_outputs %}
|
|
||||||
{% include "build/allocation_card.html" with item=item %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-danger'>
|
||||||
<b>{% trans "Create a new build output" %}</b><br>
|
{% trans "Untracked stock has not been fully allocated for this Build Order" %}
|
||||||
{% trans "No incomplete build outputs remain." %}<br>
|
</div>
|
||||||
{% trans "Create a new build output using the button above" %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<table class='table table-striped table-condensed' id='allocation-table-untracked'></table>
|
||||||
|
{% else %}
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% trans "This Build Order does not have any associated untracked BOM items" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
@ -66,19 +59,17 @@ InvenTree | Allocate Parts
|
|||||||
part: {{ build.part.pk }},
|
part: {{ build.part.pk }},
|
||||||
};
|
};
|
||||||
|
|
||||||
{% for item in build.incomplete_outputs %}
|
{% if build.has_untracked_bom_items %}
|
||||||
// Get the build output as a javascript object
|
// Load allocation table for un-tracked parts
|
||||||
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
loadBuildOutputAllocationTable(buildInfo, null);
|
||||||
{
|
{% endif %}
|
||||||
success: function(response) {
|
|
||||||
loadBuildOutputAllocationTable(buildInfo, response);
|
function reloadTable() {
|
||||||
}
|
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
);
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if build.active %}
|
{% if build.active %}
|
||||||
$("#btn-allocate").on('click', function() {
|
$("#btn-auto-allocate").on('click', function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'build-auto-allocate' build.id %}",
|
"{% url 'build-auto-allocate' build.id %}",
|
||||||
{
|
{
|
||||||
@ -86,20 +77,12 @@ InvenTree | Allocate Parts
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#btn-unallocate').on('click', function() {
|
$('#btn-unallocate').on('click', function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'build-unallocate' build.id %}",
|
"{% url 'build-unallocate' build.id %}",
|
||||||
{
|
{
|
||||||
reload: true,
|
success: reloadTable,
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#btn-create-output').click(function() {
|
|
||||||
launchModalForm('{% url "build-output-create" build.id %}',
|
|
||||||
{
|
|
||||||
reload: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -7,23 +7,31 @@
|
|||||||
<div class="panel-heading" role="tab" id="heading-{{ pk }}">
|
<div class="panel-heading" role="tab" id="heading-{{ pk }}">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
|
{% if tracked_items %}
|
||||||
<a class='collapsed' aria-expanded='false' role="button" data-toggle="collapse" data-parent="#build-output-accordion" href="#collapse-{{ pk }}" aria-controls="collapse-{{ pk }}">
|
<a class='collapsed' aria-expanded='false' role="button" data-toggle="collapse" data-parent="#build-output-accordion" href="#collapse-{{ pk }}" aria-controls="collapse-{{ pk }}">
|
||||||
|
{% endif %}
|
||||||
<div class='col-sm-4'>
|
<div class='col-sm-4'>
|
||||||
|
{% if tracked_items %}
|
||||||
<span class='fas fa-caret-right'></span>
|
<span class='fas fa-caret-right'></span>
|
||||||
|
{% endif %}
|
||||||
{{ item.part.full_name }}
|
{{ item.part.full_name }}
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-2'>
|
<div class='col-sm-2'>
|
||||||
{% if item.serial %}
|
{% if item.serial %}
|
||||||
# {{ item.serial }}
|
{% trans "Serial Number" %}: {{ item.serial }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% decimal item.quantity %}
|
{% trans "Quantity" %}: {% decimal item.quantity %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if tracked_items %}
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
<div class='col-sm-3'>
|
<div class='col-sm-3'>
|
||||||
<div>
|
<div>
|
||||||
<div id='output-progress-{{ pk }}'>
|
<div id='output-progress-{{ pk }}'>
|
||||||
|
{% if tracked_items %}
|
||||||
<span class='fas fa-spin fa-spinner'></span>
|
<span class='fas fa-spin fa-spinner'></span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,24 +3,52 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load status_codes %}
|
{% load status_codes %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Build Order" %} - {{ build }}
|
{% inventree_title %} | {% trans "Build Order" %} - {{ build }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pre_content %}
|
{% block header_pre_content %}
|
||||||
{% if build.sales_order %}
|
{% if build.sales_order %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
{% trans "This Build Order is allocated to Sales Order" %} <b><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a></b>
|
{% object_link 'so-detail' build.sales_order.id build.sales_order as link %}
|
||||||
|
{% blocktrans %}This Build Order is allocated to Sales Order {{link}}{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if build.parent %}
|
{% if build.parent %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
{% trans "This Build Order is a child of Build Order" %} <b><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a></b>
|
{% object_link 'build-detail' build.parent.id build.parent as link %}
|
||||||
|
{% blocktrans %}This Build Order is a child of Build Order {{link}}{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_post_content %}
|
||||||
|
{% if build.active %}
|
||||||
|
{% if build.can_complete %}
|
||||||
|
<div class='alert alert-block alert-success'>
|
||||||
|
{% trans "Build Order is ready to mark as completed" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if build.incomplete_count > 0 %}
|
||||||
|
<div class='alert alert-block alert-danger'>
|
||||||
|
{% trans "Build Order cannot be completed as outstanding outputs remain" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if build.completed < build.quantity %}
|
||||||
|
<div class='alert alert-block alert-warning'>
|
||||||
|
{% trans "Required build quantity has not yet been completed" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if not build.areUntrackedPartsFullyAllocated %}
|
||||||
|
<div class='alert alert-block alert-warning'>
|
||||||
|
{% trans "Stock has not been fully allocated to this Build Order" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<img class="part-thumb"
|
<img class="part-thumb"
|
||||||
{% if build.part.image %}
|
{% if build.part.image %}
|
||||||
@ -58,6 +86,11 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</div>
|
</div>
|
||||||
<!-- Build actions -->
|
<!-- Build actions -->
|
||||||
{% if roles.build.change %}
|
{% if roles.build.change %}
|
||||||
|
{% if build.active %}
|
||||||
|
<button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
|
||||||
|
<span class='fas fa-paper-plane'></span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
|
<button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
|
||||||
<span class='fas fa-tools'></span> <span class='caret'></span>
|
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||||
@ -65,12 +98,11 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<ul class='dropdown-menu' role='menu'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
<li><a href='#' id='build-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit Build" %}</a></li>
|
<li><a href='#' id='build-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit Build" %}</a></li>
|
||||||
{% if build.is_active %}
|
{% if build.is_active %}
|
||||||
<li><a href='#' id='build-complete'><span class='fas fa-tools'></span> {% trans "Complete Build" %}</a></li>
|
|
||||||
<li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
|
<li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
|
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
|
||||||
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build"% }</a>
|
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build"% }</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -105,7 +137,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>
|
<td>
|
||||||
{{ build.target_date }}
|
{{ build.target_date }}
|
||||||
{% if build.is_overdue %}
|
{% if build.is_overdue %}
|
||||||
<span title='{% trans "This build was due on" %} {{ build.target_date }}' class='label label-red'>{% trans "Overdue" %}</span>
|
<span title='{% blocktrans with target=build.target_date %}This build was due on {{target}}{% endblocktrans %}' class='label label-red'>{% trans "Overdue" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -169,6 +201,13 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#build-complete").on('click', function() {
|
$("#build-complete").on('click', function() {
|
||||||
|
|
||||||
|
{% if build.incomplete_count > 0 %}
|
||||||
|
showAlertDialog(
|
||||||
|
'{% trans "Incomplete Outputs" %}',
|
||||||
|
'{% trans "Build Order cannot be completed as incomplete build outputs remain" %}'
|
||||||
|
);
|
||||||
|
{% else %}
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'build-complete' build.id %}",
|
"{% url 'build-complete' build.id %}",
|
||||||
{
|
{
|
||||||
@ -176,6 +215,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
submit_text: '{% trans "Complete Build" %}',
|
submit_text: '{% trans "Complete Build" %}',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#print-build-report').click(function() {
|
$('#print-build-report').click(function() {
|
||||||
|
@ -6,19 +6,68 @@
|
|||||||
{% include "build/navbar.html" with tab='output' %}
|
{% include "build/navbar.html" with tab='output' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block content_panels %}
|
||||||
{% trans "Build Outputs" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block details %}
|
{% if not build.is_complete %}
|
||||||
|
<div class='panel panel-default panel-inventree'>
|
||||||
|
<div class='panel-heading'>
|
||||||
|
<h4>
|
||||||
|
{% trans "Incomplete Build Outputs" %}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "stock_table.html" with read_only=True %}
|
<div class='panel-content'>
|
||||||
|
<div class='btn-group' role='group'>
|
||||||
|
{% if build.active %}
|
||||||
|
<button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
|
||||||
|
<span class='fas fa-plus-circle'></span> {% trans "Create New Output" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if build.incomplete_outputs %}
|
||||||
|
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
||||||
|
{% for item in build.incomplete_outputs %}
|
||||||
|
{% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
<b>{% trans "Create a new build output" %}</b><br>
|
||||||
|
{% trans "No incomplete build outputs remain." %}<br>
|
||||||
|
{% trans "Create a new build output using the button above" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class='panel panel-default panel-inventree'>
|
||||||
|
<div class='panel-heading'>
|
||||||
|
<h4>
|
||||||
|
{% trans "Completed Build Outputs" %}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='panel-content'>
|
||||||
|
{% include "stock_table.html" with read_only=True %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
$('#btn-create-output').click(function() {
|
||||||
|
launchModalForm('{% url "build-output-create" build.id %}',
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
loadStockTable($("#stock-table"), {
|
loadStockTable($("#stock-table"), {
|
||||||
params: {
|
params: {
|
||||||
location_detail: true,
|
location_detail: true,
|
||||||
@ -32,4 +81,23 @@ loadStockTable($("#stock-table"), {
|
|||||||
url: "{% url 'api-stock-list' %}",
|
url: "{% url 'api-stock-list' %}",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var buildInfo = {
|
||||||
|
pk: {{ build.pk }},
|
||||||
|
quantity: {{ build.quantity }},
|
||||||
|
completed: {{ build.completed }},
|
||||||
|
part: {{ build.part.pk }},
|
||||||
|
};
|
||||||
|
|
||||||
|
{% for item in build.incomplete_outputs %}
|
||||||
|
// Get the build output as a javascript object
|
||||||
|
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
loadBuildOutputAllocationTable(buildInfo, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
{% if build.can_complete %}
|
{% if build.can_complete %}
|
||||||
<div class='alert alert-block alert-success'>
|
<div class='alert alert-block alert-success'>
|
||||||
{% trans "Build can be completed" %}
|
{% trans "Build Order is complete" %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class='alert alert-block alert-danger'>
|
<div class='alert alert-block alert-danger'>
|
||||||
<b>{% trans "Build cannot be completed" %}</b><br>
|
<b>{% trans "Build Order is incomplete" %}</b><br>
|
||||||
<ul>
|
<ul>
|
||||||
{% if build.incomplete_count > 0 %}
|
{% if build.incomplete_count > 0 %}
|
||||||
<li>{% trans "Incompleted build outputs remain" %}</li>
|
<li>{% trans "Incompleted build outputs remain" %}</li>
|
||||||
@ -17,6 +17,9 @@
|
|||||||
{% if build.completed < build.quantity %}
|
{% if build.completed < build.quantity %}
|
||||||
<li>{% trans "Required build quantity has not been completed" %}</li>
|
<li>{% trans "Required build quantity has not been completed" %}</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not build.areUntrackedPartsFullyAllocated %}
|
||||||
|
<li>{% trans "Required stock has not been fully allocated" %}</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
{% if fully_allocated %}
|
{% if not build.has_tracked_bom_items %}
|
||||||
<div class='alert alert-block alert-info'>
|
{% elif fully_allocated %}
|
||||||
<h4>{% trans "Stock allocation is complete" %}</h4>
|
<div class='alert alert-block alert-success'>
|
||||||
|
{% trans "Stock allocation is complete for this output" %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class='alert alert-block alert-danger'>
|
<div class='alert alert-block alert-danger'>
|
||||||
@ -16,7 +17,7 @@
|
|||||||
<div class='panel panel-default'>
|
<div class='panel panel-default'>
|
||||||
<div class='panel panel-heading'>
|
<div class='panel panel-heading'>
|
||||||
<a data-toggle='collapse' href='#collapse-unallocated'>
|
<a data-toggle='collapse' href='#collapse-unallocated'>
|
||||||
{{ unallocated_parts|length }} {% trans "parts have not been fully allocated" %}
|
{{ unallocated_parts|length }} {% trans "tracked parts have not been fully allocated" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-collapse collapse' id='collapse-unallocated'>
|
<div class='panel-collapse collapse' id='collapse-unallocated'>
|
||||||
@ -41,7 +42,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% include "hover_image.html" with image=build.part.image hover=True %}
|
{% include "hover_image.html" with image=build.part.image hover=True %}
|
||||||
|
{% if output.serialized %}
|
||||||
|
{{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
|
||||||
|
{% else %}
|
||||||
{% decimal output.quantity %} x {{ output.part.full_name }}
|
{% decimal output.quantity %} x {{ output.part.full_name }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -8,15 +8,13 @@
|
|||||||
</p>
|
</p>
|
||||||
{% if output %}
|
{% if output %}
|
||||||
<p>
|
<p>
|
||||||
{% trans "The allocated stock will be installed into the following build output:" %}
|
{% blocktrans %}The allocated stock will be installed into the following build output:<br><i>{{output}}</i>{% endblocktrans %}
|
||||||
<br>
|
|
||||||
<i>{{ output }}</i>
|
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if no_stock %}
|
{% if no_stock %}
|
||||||
<div class='alert alert-danger alert-block' role='alert'>
|
<div class='alert alert-danger alert-block' role='alert'>
|
||||||
{% trans "No stock available for" %} {{ part }}
|
{% blocktrans %}No stock available for {{part}}{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -5,7 +5,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Build Orders" %}
|
{% inventree_title %} | {% trans "Build Orders" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -17,17 +17,11 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if build.active %}
|
{% if build.active %}
|
||||||
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Required Parts" %}'>
|
|
||||||
<a href='{% url "build-parts" build.id %}'>
|
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "Allocate Stock" %}'>
|
||||||
<span class='fas fa-shapes'></span>
|
|
||||||
{% trans "Required Parts" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "In Progress" %}'>
|
|
||||||
<a href='{% url "build-allocate" build.id %}'>
|
<a href='{% url "build-allocate" build.id %}'>
|
||||||
<span class='fas fa-tools'></span>
|
<span class='fas fa-tools'></span>
|
||||||
{% trans "In Progress" %}
|
{% trans "Allocate Stock" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
{% extends "build/build_base.html" %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load status_codes %}
|
|
||||||
|
|
||||||
{% block menubar %}
|
|
||||||
{% include "build/navbar.html" with tab='parts' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block heading %}
|
|
||||||
{% trans "Required Parts" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block details %}
|
|
||||||
<table class='table table-striped table-condensed' id='parts-table'></table>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js_ready %}
|
|
||||||
|
|
||||||
{{ block.super }}
|
|
||||||
|
|
||||||
loadBuildPartsTable($('#parts-table'), {
|
|
||||||
part: {{ build.part.pk }},
|
|
||||||
build: {{ build.pk }},
|
|
||||||
build_quantity: {{ build.quantity }},
|
|
||||||
build_remaining: {{ build.remaining }},
|
|
||||||
});
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -19,6 +19,18 @@ class BuildTest(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Initialize data to use for these tests.
|
Initialize data to use for these tests.
|
||||||
|
|
||||||
|
The base Part 'assembly' has a BOM consisting of three parts:
|
||||||
|
|
||||||
|
- 5 x sub_part_1
|
||||||
|
- 3 x sub_part_2
|
||||||
|
- 2 x sub_part_3 (trackable)
|
||||||
|
|
||||||
|
We will build 10x 'assembly' parts, in two build outputs:
|
||||||
|
|
||||||
|
- 3 x output_1
|
||||||
|
- 7 x output_2
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create a base "Part"
|
# Create a base "Part"
|
||||||
@ -41,17 +53,31 @@ class BuildTest(TestCase):
|
|||||||
component=True
|
component=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.sub_part_3 = Part.objects.create(
|
||||||
|
name="Widget C",
|
||||||
|
description="A widget",
|
||||||
|
component=True,
|
||||||
|
trackable=True
|
||||||
|
)
|
||||||
|
|
||||||
# Create BOM item links for the parts
|
# Create BOM item links for the parts
|
||||||
BomItem.objects.create(
|
BomItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
sub_part=self.sub_part_1,
|
sub_part=self.sub_part_1,
|
||||||
quantity=10
|
quantity=5
|
||||||
)
|
)
|
||||||
|
|
||||||
BomItem.objects.create(
|
BomItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
sub_part=self.sub_part_2,
|
sub_part=self.sub_part_2,
|
||||||
quantity=25
|
quantity=3
|
||||||
|
)
|
||||||
|
|
||||||
|
# sub_part_3 is trackable!
|
||||||
|
BomItem.objects.create(
|
||||||
|
part=self.assembly,
|
||||||
|
sub_part=self.sub_part_3,
|
||||||
|
quantity=2
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a "Build" object to make 10x objects
|
# Create a "Build" object to make 10x objects
|
||||||
@ -64,14 +90,14 @@ class BuildTest(TestCase):
|
|||||||
# Create some build output (StockItem) objects
|
# Create some build output (StockItem) objects
|
||||||
self.output_1 = StockItem.objects.create(
|
self.output_1 = StockItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
quantity=5,
|
quantity=3,
|
||||||
is_building=True,
|
is_building=True,
|
||||||
build=self.build
|
build=self.build
|
||||||
)
|
)
|
||||||
|
|
||||||
self.output_2 = StockItem.objects.create(
|
self.output_2 = StockItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
quantity=5,
|
quantity=7,
|
||||||
is_building=True,
|
is_building=True,
|
||||||
build=self.build,
|
build=self.build,
|
||||||
)
|
)
|
||||||
@ -82,10 +108,12 @@ class BuildTest(TestCase):
|
|||||||
|
|
||||||
self.stock_2_1 = StockItem.objects.create(part=self.sub_part_2, quantity=5000)
|
self.stock_2_1 = StockItem.objects.create(part=self.sub_part_2, quantity=5000)
|
||||||
|
|
||||||
|
self.stock_3_1 = StockItem.objects.create(part=self.sub_part_3, quantity=1000)
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
# Perform some basic tests before we start the ball rolling
|
# Perform some basic tests before we start the ball rolling
|
||||||
|
|
||||||
self.assertEqual(StockItem.objects.count(), 5)
|
self.assertEqual(StockItem.objects.count(), 6)
|
||||||
|
|
||||||
# Build is PENDING
|
# Build is PENDING
|
||||||
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
|
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
|
||||||
@ -100,10 +128,10 @@ class BuildTest(TestCase):
|
|||||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1))
|
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1))
|
||||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
||||||
|
|
||||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 50)
|
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 15)
|
||||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_2), 50)
|
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_2), 35)
|
||||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_1), 125)
|
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_1), 9)
|
||||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_2), 125)
|
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_2), 21)
|
||||||
|
|
||||||
self.assertFalse(self.build.is_complete)
|
self.assertFalse(self.build.is_complete)
|
||||||
|
|
||||||
@ -144,84 +172,113 @@ class BuildTest(TestCase):
|
|||||||
quantity=99
|
quantity=99
|
||||||
)
|
)
|
||||||
|
|
||||||
def allocate_stock(self, q11, q12, q21, output):
|
def allocate_stock(self, output, allocations):
|
||||||
# Assign stock to this build
|
"""
|
||||||
|
Allocate stock to this build, against a particular output
|
||||||
|
|
||||||
if q11 > 0:
|
Args:
|
||||||
|
output - StockItem object (or None)
|
||||||
|
allocations - Map of {StockItem: quantity}
|
||||||
|
"""
|
||||||
|
|
||||||
|
for item, quantity in allocations.items():
|
||||||
BuildItem.objects.create(
|
BuildItem.objects.create(
|
||||||
build=self.build,
|
build=self.build,
|
||||||
stock_item=self.stock_1_1,
|
stock_item=item,
|
||||||
quantity=q11,
|
quantity=quantity,
|
||||||
install_into=output
|
install_into=output
|
||||||
)
|
)
|
||||||
|
|
||||||
if q12 > 0:
|
|
||||||
BuildItem.objects.create(
|
|
||||||
build=self.build,
|
|
||||||
stock_item=self.stock_1_2,
|
|
||||||
quantity=q12,
|
|
||||||
install_into=output
|
|
||||||
)
|
|
||||||
|
|
||||||
if q21 > 0:
|
|
||||||
BuildItem.objects.create(
|
|
||||||
build=self.build,
|
|
||||||
stock_item=self.stock_2_1,
|
|
||||||
quantity=q21,
|
|
||||||
install_into=output,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attempt to create another identical BuildItem
|
|
||||||
b = BuildItem(
|
|
||||||
build=self.build,
|
|
||||||
stock_item=self.stock_2_1,
|
|
||||||
quantity=q21
|
|
||||||
)
|
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
|
||||||
b.clean()
|
|
||||||
|
|
||||||
def test_partial_allocation(self):
|
def test_partial_allocation(self):
|
||||||
"""
|
"""
|
||||||
Partially allocate against output 1
|
Test partial allocation of stock
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.allocate_stock(50, 50, 200, self.output_1)
|
# Fully allocate tracked stock against build output 1
|
||||||
|
self.allocate_stock(
|
||||||
|
self.output_1,
|
||||||
|
{
|
||||||
|
self.stock_3_1: 6,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
||||||
|
|
||||||
|
# Partially allocate tracked stock against build output 2
|
||||||
|
self.allocate_stock(
|
||||||
|
self.output_2,
|
||||||
|
{
|
||||||
|
self.stock_3_1: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.assertFalse(self.build.isFullyAllocated(self.output_2))
|
self.assertFalse(self.build.isFullyAllocated(self.output_2))
|
||||||
self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1))
|
|
||||||
self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1))
|
|
||||||
|
|
||||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_2))
|
|
||||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
|
||||||
|
|
||||||
# Check that the part has been allocated
|
# Partially allocate untracked stock against build
|
||||||
self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 100)
|
self.allocate_stock(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
self.stock_1_1: 1,
|
||||||
|
self.stock_2_1: 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.build.unallocateStock(output=self.output_1)
|
self.assertFalse(self.build.isFullyAllocated(None, verbose=True))
|
||||||
self.assertEqual(BuildItem.objects.count(), 0)
|
|
||||||
|
|
||||||
# Check that the part has been unallocated
|
unallocated = self.build.unallocatedParts(None)
|
||||||
self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 0)
|
|
||||||
|
self.assertEqual(len(unallocated), 2)
|
||||||
|
|
||||||
|
self.allocate_stock(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
self.stock_1_2: 100,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(self.build.isFullyAllocated(None, verbose=True))
|
||||||
|
|
||||||
|
unallocated = self.build.unallocatedParts(None)
|
||||||
|
|
||||||
|
self.assertEqual(len(unallocated), 1)
|
||||||
|
|
||||||
|
self.build.unallocateUntracked()
|
||||||
|
|
||||||
|
unallocated = self.build.unallocatedParts(None)
|
||||||
|
|
||||||
|
self.assertEqual(len(unallocated), 2)
|
||||||
|
|
||||||
|
self.assertFalse(self.build.areUntrackedPartsFullyAllocated())
|
||||||
|
|
||||||
|
# Now we "fully" allocate the untracked untracked items
|
||||||
|
self.allocate_stock(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
self.stock_1_1: 50,
|
||||||
|
self.stock_2_1: 50,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(self.build.areUntrackedPartsFullyAllocated())
|
||||||
|
|
||||||
def test_auto_allocate(self):
|
def test_auto_allocate(self):
|
||||||
"""
|
"""
|
||||||
Test auto-allocation functionality against the build outputs
|
Test auto-allocation functionality against the build outputs.
|
||||||
|
|
||||||
|
Note: auto-allocations only work for un-tracked stock!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allocations = self.build.getAutoAllocations(self.output_1)
|
allocations = self.build.getAutoAllocations()
|
||||||
|
|
||||||
self.assertEqual(len(allocations), 1)
|
self.assertEqual(len(allocations), 1)
|
||||||
|
|
||||||
self.build.autoAllocate(self.output_1)
|
self.build.autoAllocate()
|
||||||
self.assertEqual(BuildItem.objects.count(), 1)
|
self.assertEqual(BuildItem.objects.count(), 1)
|
||||||
|
|
||||||
# Check that one part has been fully allocated to the build output
|
# Check that one un-tracked part has been fully allocated to the build
|
||||||
self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1))
|
self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, None))
|
||||||
|
|
||||||
# But, the *other* build output has not been allocated against
|
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, None))
|
||||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
|
||||||
|
|
||||||
def test_cancel(self):
|
def test_cancel(self):
|
||||||
"""
|
"""
|
||||||
@ -243,9 +300,33 @@ class BuildTest(TestCase):
|
|||||||
Test completion of a build output
|
Test completion of a build output
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.allocate_stock(50, 50, 250, self.output_1)
|
# Allocate non-tracked parts
|
||||||
self.allocate_stock(50, 50, 250, self.output_2)
|
self.allocate_stock(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
self.stock_1_1: self.stock_1_1.quantity, # Allocate *all* stock from this item
|
||||||
|
self.stock_1_2: 10,
|
||||||
|
self.stock_2_1: 30
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Allocate tracked parts to output_1
|
||||||
|
self.allocate_stock(
|
||||||
|
self.output_1,
|
||||||
|
{
|
||||||
|
self.stock_3_1: 6
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Allocate tracked parts to output_2
|
||||||
|
self.allocate_stock(
|
||||||
|
self.output_2,
|
||||||
|
{
|
||||||
|
self.stock_3_1: 14
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(self.build.isFullyAllocated(None, verbose=True))
|
||||||
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
||||||
self.assertTrue(self.build.isFullyAllocated(self.output_2))
|
self.assertTrue(self.build.isFullyAllocated(self.output_2))
|
||||||
|
|
||||||
@ -265,19 +346,16 @@ class BuildTest(TestCase):
|
|||||||
self.assertEqual(BuildItem.objects.count(), 0)
|
self.assertEqual(BuildItem.objects.count(), 0)
|
||||||
|
|
||||||
# New stock items should have been created!
|
# New stock items should have been created!
|
||||||
self.assertEqual(StockItem.objects.count(), 4)
|
self.assertEqual(StockItem.objects.count(), 7)
|
||||||
|
|
||||||
a = StockItem.objects.get(pk=self.stock_1_1.pk)
|
|
||||||
|
|
||||||
# This stock item has been depleted!
|
# This stock item has been depleted!
|
||||||
with self.assertRaises(StockItem.DoesNotExist):
|
with self.assertRaises(StockItem.DoesNotExist):
|
||||||
StockItem.objects.get(pk=self.stock_1_2.pk)
|
StockItem.objects.get(pk=self.stock_1_1.pk)
|
||||||
|
|
||||||
c = StockItem.objects.get(pk=self.stock_2_1.pk)
|
# This stock item has *not* been depleted
|
||||||
|
x = StockItem.objects.get(pk=self.stock_2_1.pk)
|
||||||
|
|
||||||
# Stock should have been subtracted from the original items
|
self.assertEqual(x.quantity, 4970)
|
||||||
self.assertEqual(a.quantity, 900)
|
|
||||||
self.assertEqual(c.quantity, 4500)
|
|
||||||
|
|
||||||
# And 10 new stock items created for the build output
|
# And 10 new stock items created for the build output
|
||||||
outputs = StockItem.objects.filter(build=self.build)
|
outputs = StockItem.objects.filter(build=self.build)
|
||||||
|
@ -15,7 +15,7 @@ from datetime import datetime, timedelta
|
|||||||
from .models import Build
|
from .models import Build
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||||
|
|
||||||
|
|
||||||
class BuildTestSimple(TestCase):
|
class BuildTestSimple(TestCase):
|
||||||
@ -335,6 +335,7 @@ class TestBuildViews(TestCase):
|
|||||||
'confirm_incomplete': 1,
|
'confirm_incomplete': 1,
|
||||||
'location': 1,
|
'location': 1,
|
||||||
'output': self.output.pk,
|
'output': self.output.pk,
|
||||||
|
'stock_status': StockStatus.DAMAGED
|
||||||
},
|
},
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||||
)
|
)
|
||||||
@ -342,6 +343,7 @@ class TestBuildViews(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
|
|
||||||
self.assertTrue(data['form_valid'])
|
self.assertTrue(data['form_valid'])
|
||||||
|
|
||||||
# Now the build should be able to be completed
|
# Now the build should be able to be completed
|
||||||
|
@ -21,7 +21,6 @@ build_detail_urls = [
|
|||||||
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
||||||
|
|
||||||
url(r'^children/', views.BuildDetail.as_view(template_name='build/build_children.html'), name='build-children'),
|
url(r'^children/', views.BuildDetail.as_view(template_name='build/build_children.html'), name='build-children'),
|
||||||
url(r'^parts/', views.BuildDetail.as_view(template_name='build/parts.html'), name='build-parts'),
|
|
||||||
url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'),
|
url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'),
|
||||||
url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
|
url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@ from stock.models import StockLocation, StockItem
|
|||||||
|
|
||||||
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
|
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
from InvenTree.helpers import str2bool, extract_serial_numbers, normalize
|
from InvenTree.helpers import str2bool, extract_serial_numbers, normalize, isNull
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||||
|
|
||||||
|
|
||||||
class BuildIndex(InvenTreeRoleMixin, ListView):
|
class BuildIndex(InvenTreeRoleMixin, ListView):
|
||||||
@ -98,16 +98,6 @@ class BuildAutoAllocate(AjaxUpdateView):
|
|||||||
|
|
||||||
initials = super().get_initial()
|
initials = super().get_initial()
|
||||||
|
|
||||||
# Pointing to a particular build output?
|
|
||||||
output = self.get_param('output')
|
|
||||||
|
|
||||||
if output:
|
|
||||||
try:
|
|
||||||
output = StockItem.objects.get(pk=output)
|
|
||||||
initials['output'] = output
|
|
||||||
except (ValueError, StockItem.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
@ -119,18 +109,7 @@ class BuildAutoAllocate(AjaxUpdateView):
|
|||||||
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
form = self.get_form()
|
context['allocations'] = build.getAutoAllocations()
|
||||||
|
|
||||||
output_id = form['output'].value()
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = StockItem.objects.get(pk=output_id)
|
|
||||||
except (ValueError, StockItem.DoesNotExist):
|
|
||||||
output = None
|
|
||||||
|
|
||||||
if output:
|
|
||||||
context['output'] = output
|
|
||||||
context['allocations'] = build.getAutoAllocations(output)
|
|
||||||
|
|
||||||
context['build'] = build
|
context['build'] = build
|
||||||
|
|
||||||
@ -140,18 +119,11 @@ class BuildAutoAllocate(AjaxUpdateView):
|
|||||||
|
|
||||||
form = super().get_form()
|
form = super().get_form()
|
||||||
|
|
||||||
if form['output'].value():
|
|
||||||
# Hide the 'output' field
|
|
||||||
form.fields['output'].widget = HiddenInput()
|
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def validate(self, build, form, **kwargs):
|
def validate(self, build, form, **kwargs):
|
||||||
|
|
||||||
output = form.cleaned_data.get('output', None)
|
pass
|
||||||
|
|
||||||
if not output:
|
|
||||||
form.add_error(None, _('Build output must be specified'))
|
|
||||||
|
|
||||||
def save(self, build, form, **kwargs):
|
def save(self, build, form, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -159,9 +131,7 @@ class BuildAutoAllocate(AjaxUpdateView):
|
|||||||
perform auto-allocations
|
perform auto-allocations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
output = form.cleaned_data.get('output', None)
|
build.autoAllocate()
|
||||||
|
|
||||||
build.autoAllocate(output)
|
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
@ -242,7 +212,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
|
|
||||||
# Calculate the required quantity
|
# Calculate the required quantity
|
||||||
quantity = max(0, build.remaining - build.incomplete_count)
|
quantity = max(0, build.remaining - build.incomplete_count)
|
||||||
initials['quantity'] = quantity
|
initials['output_quantity'] = quantity
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
@ -365,10 +335,16 @@ class BuildUnallocate(AjaxUpdateView):
|
|||||||
|
|
||||||
output_id = request.POST.get('output_id', None)
|
output_id = request.POST.get('output_id', None)
|
||||||
|
|
||||||
try:
|
if output_id:
|
||||||
output = StockItem.objects.get(pk=output_id)
|
|
||||||
except (ValueError, StockItem.DoesNotExist):
|
# If a "null" output is provided, we are trying to unallocate "untracked" stock
|
||||||
output = None
|
if isNull(output_id):
|
||||||
|
output = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
output = StockItem.objects.get(pk=output_id)
|
||||||
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
|
output = None
|
||||||
|
|
||||||
part_id = request.POST.get('part_id', None)
|
part_id = request.POST.get('part_id', None)
|
||||||
|
|
||||||
@ -383,9 +359,19 @@ class BuildUnallocate(AjaxUpdateView):
|
|||||||
form.add_error('confirm', _('Confirm unallocation of build stock'))
|
form.add_error('confirm', _('Confirm unallocation of build stock'))
|
||||||
form.add_error(None, _('Check the confirmation box'))
|
form.add_error(None, _('Check the confirmation box'))
|
||||||
else:
|
else:
|
||||||
build.unallocateStock(output=output, part=part)
|
|
||||||
valid = True
|
valid = True
|
||||||
|
|
||||||
|
# Unallocate the entire build
|
||||||
|
if not output_id:
|
||||||
|
build.unallocateAll()
|
||||||
|
# Unallocate a single output
|
||||||
|
elif output:
|
||||||
|
build.unallocateOutput(output, part=part)
|
||||||
|
# Unallocate "untracked" parts
|
||||||
|
else:
|
||||||
|
build.unallocateUntracked(part=part)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'form_valid': valid,
|
'form_valid': valid,
|
||||||
}
|
}
|
||||||
@ -410,8 +396,8 @@ class BuildComplete(AjaxUpdateView):
|
|||||||
|
|
||||||
def validate(self, build, form, **kwargs):
|
def validate(self, build, form, **kwargs):
|
||||||
|
|
||||||
if not build.can_complete:
|
if build.incomplete_count > 0:
|
||||||
form.add_error(None, _('Build order cannot be completed'))
|
form.add_error(None, _('Build order cannot be completed - incomplete outputs remain'))
|
||||||
|
|
||||||
def save(self, build, form, **kwargs):
|
def save(self, build, form, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -431,7 +417,7 @@ class BuildOutputComplete(AjaxUpdateView):
|
|||||||
View to mark a particular build output as Complete.
|
View to mark a particular build output as Complete.
|
||||||
|
|
||||||
- Notifies the user of which parts will be removed from stock.
|
- Notifies the user of which parts will be removed from stock.
|
||||||
- Removes allocated items from stock
|
- Assignes (tracked) allocated items from stock to the build output
|
||||||
- Deletes pending BuildItem objects
|
- Deletes pending BuildItem objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -463,11 +449,25 @@ class BuildOutputComplete(AjaxUpdateView):
|
|||||||
return form
|
return form
|
||||||
|
|
||||||
def validate(self, build, form, **kwargs):
|
def validate(self, build, form, **kwargs):
|
||||||
|
"""
|
||||||
|
Custom validation steps for the BuildOutputComplete" form
|
||||||
|
"""
|
||||||
|
|
||||||
data = form.cleaned_data
|
data = form.cleaned_data
|
||||||
|
|
||||||
output = data.get('output', None)
|
output = data.get('output', None)
|
||||||
|
|
||||||
|
stock_status = data.get('stock_status', StockStatus.OK)
|
||||||
|
|
||||||
|
# Any "invalid" stock status defaults to OK
|
||||||
|
try:
|
||||||
|
stock_status = int(stock_status)
|
||||||
|
except (ValueError):
|
||||||
|
stock_status = StockStatus.OK
|
||||||
|
|
||||||
|
if int(stock_status) not in StockStatus.keys():
|
||||||
|
form.add_error('stock_status', _('Invalid stock status value selected'))
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
|
|
||||||
quantity = data.get('quantity', None)
|
quantity = data.get('quantity', None)
|
||||||
@ -559,12 +559,20 @@ class BuildOutputComplete(AjaxUpdateView):
|
|||||||
|
|
||||||
location = data.get('location', None)
|
location = data.get('location', None)
|
||||||
output = data.get('output', None)
|
output = data.get('output', None)
|
||||||
|
stock_status = data.get('stock_status', StockStatus.OK)
|
||||||
|
|
||||||
|
# Any "invalid" stock status defaults to OK
|
||||||
|
try:
|
||||||
|
stock_status = int(stock_status)
|
||||||
|
except (ValueError):
|
||||||
|
stock_status = StockStatus.OK
|
||||||
|
|
||||||
# Complete the build output
|
# Complete the build output
|
||||||
build.completeBuildOutput(
|
build.completeBuildOutput(
|
||||||
output,
|
output,
|
||||||
self.request.user,
|
self.request.user,
|
||||||
location=location,
|
location=location,
|
||||||
|
status=stock_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
@ -632,10 +640,12 @@ class BuildAllocate(InvenTreeRoleMixin, DetailView):
|
|||||||
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
part = build.part
|
part = build.part
|
||||||
bom_items = part.bom_items
|
bom_items = build.bom_items
|
||||||
|
|
||||||
context['part'] = part
|
context['part'] = part
|
||||||
context['bom_items'] = bom_items
|
context['bom_items'] = bom_items
|
||||||
|
context['has_tracked_bom_items'] = build.has_tracked_bom_items()
|
||||||
|
context['has_untracked_bom_items'] = build.has_untracked_bom_items()
|
||||||
context['BuildStatus'] = BuildStatus
|
context['BuildStatus'] = BuildStatus
|
||||||
|
|
||||||
context['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
context['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
||||||
|
@ -58,6 +58,13 @@ class InvenTreeSetting(models.Model):
|
|||||||
'description': _('String descriptor for the server instance'),
|
'description': _('String descriptor for the server instance'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'INVENTREE_INSTANCE_TITLE': {
|
||||||
|
'name': _('Use instance name'),
|
||||||
|
'description': _('Use the instance name in the title-bar'),
|
||||||
|
'validator': bool,
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
|
||||||
'INVENTREE_COMPANY_NAME': {
|
'INVENTREE_COMPANY_NAME': {
|
||||||
'name': _('Company name'),
|
'name': _('Company name'),
|
||||||
'description': _('Internal company name'),
|
'description': _('Internal company name'),
|
||||||
|
@ -15,9 +15,11 @@ from django.db.models import Q
|
|||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import ManufacturerPart
|
||||||
from .models import SupplierPart, SupplierPriceBreak
|
from .models import SupplierPart, SupplierPriceBreak
|
||||||
|
|
||||||
from .serializers import CompanySerializer
|
from .serializers import CompanySerializer
|
||||||
|
from .serializers import ManufacturerPartSerializer
|
||||||
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
|
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -80,8 +82,105 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
queryset = CompanySerializer.annotate_queryset(queryset)
|
queryset = CompanySerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartList(generics.ListCreateAPIView):
|
||||||
|
""" API endpoint for list view of ManufacturerPart object
|
||||||
|
|
||||||
|
- GET: Return list of ManufacturerPart objects
|
||||||
|
- POST: Create a new ManufacturerPart object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = ManufacturerPart.objects.all().prefetch_related(
|
||||||
|
'part',
|
||||||
|
'manufacturer',
|
||||||
|
'supplier_parts',
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer_class = ManufacturerPartSerializer
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Do we wish to include extra detail?
|
||||||
|
try:
|
||||||
|
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
kwargs['manufacturer_detail'] = str2bool(self.request.query_params.get('manufacturer_detail', None))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
|
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('company', None)
|
||||||
|
|
||||||
|
if manufacturer is not None:
|
||||||
|
queryset = queryset.filter(manufacturer=manufacturer)
|
||||||
|
|
||||||
|
# Filter by parent part?
|
||||||
|
part = params.get('part', None)
|
||||||
|
|
||||||
|
if part is not None:
|
||||||
|
queryset = queryset.filter(part=part)
|
||||||
|
|
||||||
|
# Filter by 'active' status of the part?
|
||||||
|
active = params.get('active', None)
|
||||||
|
|
||||||
|
if active is not None:
|
||||||
|
active = str2bool(active)
|
||||||
|
queryset = queryset.filter(part__active=active)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
filter_backends = [
|
||||||
|
DjangoFilterBackend,
|
||||||
|
filters.SearchFilter,
|
||||||
|
filters.OrderingFilter,
|
||||||
|
]
|
||||||
|
|
||||||
|
filter_fields = [
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
'manufacturer__name',
|
||||||
|
'description',
|
||||||
|
'MPN',
|
||||||
|
'part__name',
|
||||||
|
'part__description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
""" API endpoint for detail view of ManufacturerPart object
|
||||||
|
|
||||||
|
- GET: Retrieve detail view
|
||||||
|
- PATCH: Update object
|
||||||
|
- DELETE: Delete object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = ManufacturerPart.objects.all()
|
||||||
|
serializer_class = ManufacturerPartSerializer
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartList(generics.ListCreateAPIView):
|
class SupplierPartList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for list view of SupplierPart object
|
""" API endpoint for list view of SupplierPart object
|
||||||
|
|
||||||
@ -92,7 +191,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
queryset = SupplierPart.objects.all().prefetch_related(
|
queryset = SupplierPart.objects.all().prefetch_related(
|
||||||
'part',
|
'part',
|
||||||
'supplier',
|
'supplier',
|
||||||
'manufacturer'
|
'manufacturer_part__manufacturer',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -114,7 +213,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
manufacturer = params.get('manufacturer', None)
|
manufacturer = params.get('manufacturer', None)
|
||||||
|
|
||||||
if manufacturer is not None:
|
if manufacturer is not None:
|
||||||
queryset = queryset.filter(manufacturer=manufacturer)
|
queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer)
|
||||||
|
|
||||||
# Filter by supplier
|
# Filter by supplier
|
||||||
supplier = params.get('supplier', None)
|
supplier = params.get('supplier', None)
|
||||||
@ -126,7 +225,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
company = params.get('company', None)
|
company = params.get('company', None)
|
||||||
|
|
||||||
if company is not None:
|
if company is not None:
|
||||||
queryset = queryset.filter(Q(manufacturer=company) | Q(supplier=company))
|
queryset = queryset.filter(Q(manufacturer_part__manufacturer=company) | Q(supplier=company))
|
||||||
|
|
||||||
# Filter by parent part?
|
# Filter by parent part?
|
||||||
part = params.get('part', None)
|
part = params.get('part', None)
|
||||||
@ -134,6 +233,12 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
if part is not None:
|
if part is not None:
|
||||||
queryset = queryset.filter(part=part)
|
queryset = queryset.filter(part=part)
|
||||||
|
|
||||||
|
# Filter by manufacturer part?
|
||||||
|
manufacturer_part = params.get('manufacturer_part', None)
|
||||||
|
|
||||||
|
if manufacturer_part is not None:
|
||||||
|
queryset = queryset.filter(manufacturer_part=manufacturer_part)
|
||||||
|
|
||||||
# Filter by 'active' status of the part?
|
# Filter by 'active' status of the part?
|
||||||
active = params.get('active', None)
|
active = params.get('active', None)
|
||||||
|
|
||||||
@ -184,9 +289,9 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
search_fields = [
|
search_fields = [
|
||||||
'SKU',
|
'SKU',
|
||||||
'supplier__name',
|
'supplier__name',
|
||||||
'manufacturer__name',
|
'manufacturer_part__manufacturer__name',
|
||||||
'description',
|
'description',
|
||||||
'MPN',
|
'manufacturer_part__MPN',
|
||||||
'part__name',
|
'part__name',
|
||||||
'part__description',
|
'part__description',
|
||||||
]
|
]
|
||||||
@ -197,7 +302,7 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
|
|
||||||
- GET: Retrieve detail view
|
- GET: Retrieve detail view
|
||||||
- PATCH: Update object
|
- PATCH: Update object
|
||||||
- DELETE: Delete objec
|
- DELETE: Delete object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = SupplierPart.objects.all()
|
queryset = SupplierPart.objects.all()
|
||||||
@ -226,6 +331,15 @@ class SupplierPriceBreakList(generics.ListCreateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
manufacturer_part_api_urls = [
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/?', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'),
|
||||||
|
|
||||||
|
# Catch anything else
|
||||||
|
url(r'^.*$', ManufacturerPartList.as_view(), name='api-manufacturer-part-list'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
supplier_part_api_urls = [
|
supplier_part_api_urls = [
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
|
url(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
|
||||||
@ -236,7 +350,8 @@ supplier_part_api_urls = [
|
|||||||
|
|
||||||
|
|
||||||
company_api_urls = [
|
company_api_urls = [
|
||||||
|
url(r'^part/manufacturer/', include(manufacturer_part_api_urls)),
|
||||||
|
|
||||||
url(r'^part/', include(supplier_part_api_urls)),
|
url(r'^part/', include(supplier_part_api_urls)),
|
||||||
|
|
||||||
url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'),
|
url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'),
|
||||||
|
@ -31,3 +31,17 @@
|
|||||||
name: Another customer!
|
name: Another customer!
|
||||||
description: Yet another company
|
description: Yet another company
|
||||||
is_customer: True
|
is_customer: True
|
||||||
|
|
||||||
|
- model: company.company
|
||||||
|
pk: 6
|
||||||
|
fields:
|
||||||
|
name: A manufacturer
|
||||||
|
description: A company that makes parts!
|
||||||
|
is_manufacturer: True
|
||||||
|
|
||||||
|
- model: company.company
|
||||||
|
pk: 7
|
||||||
|
fields:
|
||||||
|
name: Another manufacturer
|
||||||
|
description: They build things and sell it to us
|
||||||
|
is_manufacturer: True
|
||||||
|
39
InvenTree/company/fixtures/manufacturer_part.yaml
Normal file
39
InvenTree/company/fixtures/manufacturer_part.yaml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Manufacturer Parts
|
||||||
|
|
||||||
|
- model: company.manufacturerpart
|
||||||
|
pk: 1
|
||||||
|
fields:
|
||||||
|
part: 5
|
||||||
|
manufacturer: 6
|
||||||
|
MPN: 'MPN123'
|
||||||
|
|
||||||
|
- model: company.manufacturerpart
|
||||||
|
pk: 2
|
||||||
|
fields:
|
||||||
|
part: 3
|
||||||
|
manufacturer: 7
|
||||||
|
MPN: 'MPN456'
|
||||||
|
|
||||||
|
- model: company.manufacturerpart
|
||||||
|
pk: 3
|
||||||
|
fields:
|
||||||
|
part: 5
|
||||||
|
manufacturer: 7
|
||||||
|
MPN: 'MPN789'
|
||||||
|
|
||||||
|
# Supplier parts linked to Manufacturer parts
|
||||||
|
- model: company.supplierpart
|
||||||
|
pk: 10
|
||||||
|
fields:
|
||||||
|
part: 3
|
||||||
|
manufacturer_part: 2
|
||||||
|
supplier: 2
|
||||||
|
SKU: 'MPN456-APPEL'
|
||||||
|
|
||||||
|
- model: company.supplierpart
|
||||||
|
pk: 11
|
||||||
|
fields:
|
||||||
|
part: 3
|
||||||
|
manufacturer_part: 2
|
||||||
|
supplier: 3
|
||||||
|
SKU: 'MPN456-ZERG'
|
@ -17,6 +17,7 @@ from djmoney.forms.fields import MoneyField
|
|||||||
import common.settings
|
import common.settings
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import ManufacturerPart
|
||||||
from .models import SupplierPart
|
from .models import SupplierPart
|
||||||
from .models import SupplierPriceBreak
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
@ -85,12 +86,30 @@ class CompanyImageDownloadForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EditManufacturerPartForm(HelperForm):
|
||||||
|
""" Form for editing a ManufacturerPart object """
|
||||||
|
|
||||||
|
field_prefix = {
|
||||||
|
'link': 'fa-link',
|
||||||
|
'MPN': 'fa-hashtag',
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ManufacturerPart
|
||||||
|
fields = [
|
||||||
|
'part',
|
||||||
|
'manufacturer',
|
||||||
|
'MPN',
|
||||||
|
'description',
|
||||||
|
'link',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class EditSupplierPartForm(HelperForm):
|
class EditSupplierPartForm(HelperForm):
|
||||||
""" Form for editing a SupplierPart object """
|
""" Form for editing a SupplierPart object """
|
||||||
|
|
||||||
field_prefix = {
|
field_prefix = {
|
||||||
'link': 'fa-link',
|
'link': 'fa-link',
|
||||||
'MPN': 'fa-hashtag',
|
|
||||||
'SKU': 'fa-hashtag',
|
'SKU': 'fa-hashtag',
|
||||||
'note': 'fa-pencil-alt',
|
'note': 'fa-pencil-alt',
|
||||||
}
|
}
|
||||||
@ -104,15 +123,28 @@ class EditSupplierPartForm(HelperForm):
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
manufacturer = django.forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
help_text=_('Select manufacturer'),
|
||||||
|
choices=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
MPN = django.forms.CharField(
|
||||||
|
required=False,
|
||||||
|
help_text=_('Manufacturer Part Number'),
|
||||||
|
max_length=100,
|
||||||
|
label=_('MPN'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
fields = [
|
fields = [
|
||||||
'part',
|
'part',
|
||||||
'supplier',
|
'supplier',
|
||||||
'SKU',
|
'SKU',
|
||||||
'description',
|
|
||||||
'manufacturer',
|
'manufacturer',
|
||||||
'MPN',
|
'MPN',
|
||||||
|
'description',
|
||||||
'link',
|
'link',
|
||||||
'note',
|
'note',
|
||||||
'single_pricing',
|
'single_pricing',
|
||||||
@ -121,6 +153,19 @@ class EditSupplierPartForm(HelperForm):
|
|||||||
'packaging',
|
'packaging',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_manufacturer_choices(self):
|
||||||
|
""" Returns tuples for all manufacturers """
|
||||||
|
empty_choice = [('', '----------')]
|
||||||
|
|
||||||
|
manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)]
|
||||||
|
|
||||||
|
return empty_choice + manufacturers
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['manufacturer'].choices = self.get_manufacturer_choices()
|
||||||
|
|
||||||
|
|
||||||
class EditPriceBreakForm(HelperForm):
|
class EditPriceBreakForm(HelperForm):
|
||||||
""" Form for creating / editing a supplier price break """
|
""" Form for creating / editing a supplier price break """
|
||||||
|
27
InvenTree/company/migrations/0034_manufacturerpart.py
Normal file
27
InvenTree/company/migrations/0034_manufacturerpart.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import InvenTree.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('company', '0033_auto_20210410_1528'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ManufacturerPart',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('MPN', models.CharField(help_text='Manufacturer Part Number', max_length=100, null=True, verbose_name='MPN')),
|
||||||
|
('link', InvenTree.fields.InvenTreeURLField(blank=True, help_text='URL for external manufacturer part link', null=True, verbose_name='Link')),
|
||||||
|
('description', models.CharField(blank=True, help_text='Manufacturer part description', max_length=250, null=True, verbose_name='Description')),
|
||||||
|
('manufacturer', models.ForeignKey(help_text='Select manufacturer', limit_choices_to={'is_manufacturer': True}, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='manufactured_parts', to='company.Company', verbose_name='Manufacturer')),
|
||||||
|
('part', models.ForeignKey(help_text='Select part', limit_choices_to={'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='manufacturer_parts', to='part.Part', verbose_name='Base Part')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('part', 'manufacturer', 'MPN')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/company/migrations/0035_supplierpart_update_1.py
Normal file
18
InvenTree/company/migrations/0035_supplierpart_update_1.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import InvenTree.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('company', '0034_manufacturerpart'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='supplierpart',
|
||||||
|
name='manufacturer_part',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Select manufacturer part', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='company.ManufacturerPart', verbose_name='Manufacturer Part'),
|
||||||
|
),
|
||||||
|
]
|
110
InvenTree/company/migrations/0036_supplierpart_update_2.py
Normal file
110
InvenTree/company/migrations/0036_supplierpart_update_2.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import InvenTree.fields
|
||||||
|
from django.db import migrations, models, transaction
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
|
def supplierpart_make_manufacturer_parts(apps, schema_editor):
|
||||||
|
Part = apps.get_model('part', 'Part')
|
||||||
|
ManufacturerPart = apps.get_model('company', 'ManufacturerPart')
|
||||||
|
SupplierPart = apps.get_model('company', 'SupplierPart')
|
||||||
|
|
||||||
|
supplier_parts = SupplierPart.objects.all()
|
||||||
|
|
||||||
|
if supplier_parts:
|
||||||
|
print(f'\nCreating ManufacturerPart Objects\n{"-"*10}')
|
||||||
|
for supplier_part in supplier_parts:
|
||||||
|
print(f'{supplier_part.supplier.name[:15].ljust(15)} | {supplier_part.SKU[:15].ljust(15)}\t', end='')
|
||||||
|
|
||||||
|
if supplier_part.manufacturer_part:
|
||||||
|
print(f'[ERROR: MANUFACTURER PART ALREADY EXISTS]')
|
||||||
|
continue
|
||||||
|
|
||||||
|
part = supplier_part.part
|
||||||
|
if not part:
|
||||||
|
print(f'[ERROR: SUPPLIER PART IS NOT CONNECTED TO PART]')
|
||||||
|
continue
|
||||||
|
|
||||||
|
manufacturer = supplier_part.manufacturer
|
||||||
|
MPN = supplier_part.MPN
|
||||||
|
link = supplier_part.link
|
||||||
|
description = supplier_part.description
|
||||||
|
|
||||||
|
if manufacturer or MPN:
|
||||||
|
print(f' | {part.name[:15].ljust(15)}', end='')
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f' | {manufacturer.name[:15].ljust(15)}', end='')
|
||||||
|
except AttributeError:
|
||||||
|
print(f' | {"EMPTY MANUF".ljust(15)}', end='')
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f' | {MPN[:15].ljust(15)}', end='')
|
||||||
|
except TypeError:
|
||||||
|
print(f' | {"EMPTY MPN".ljust(15)}', end='')
|
||||||
|
|
||||||
|
print('\t', end='')
|
||||||
|
|
||||||
|
# Create ManufacturerPart
|
||||||
|
manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=MPN, description=description, link=link)
|
||||||
|
created = False
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
manufacturer_part.save()
|
||||||
|
created = True
|
||||||
|
except IntegrityError:
|
||||||
|
manufacturer_part = ManufacturerPart.objects.get(part=part, manufacturer=manufacturer, MPN=MPN)
|
||||||
|
|
||||||
|
# Link it to SupplierPart
|
||||||
|
supplier_part.manufacturer_part = manufacturer_part
|
||||||
|
supplier_part.save()
|
||||||
|
|
||||||
|
if created:
|
||||||
|
print(f'[SUCCESS: MANUFACTURER PART CREATED]')
|
||||||
|
else:
|
||||||
|
print(f'[IGNORED: MANUFACTURER PART ALREADY EXISTS]')
|
||||||
|
else:
|
||||||
|
print(f'[IGNORED: MISSING MANUFACTURER DATA]')
|
||||||
|
|
||||||
|
print(f'{"-"*10}\nDone\n')
|
||||||
|
|
||||||
|
def supplierpart_populate_manufacturer_info(apps, schema_editor):
|
||||||
|
Part = apps.get_model('part', 'Part')
|
||||||
|
ManufacturerPart = apps.get_model('company', 'ManufacturerPart')
|
||||||
|
SupplierPart = apps.get_model('company', 'SupplierPart')
|
||||||
|
|
||||||
|
supplier_parts = SupplierPart.objects.all()
|
||||||
|
|
||||||
|
if supplier_parts:
|
||||||
|
print(f'\nSupplierPart: Populating Manufacturer Information\n{"-"*10}')
|
||||||
|
for supplier_part in supplier_parts:
|
||||||
|
print(f'{supplier_part.supplier.name[:15].ljust(15)} | {supplier_part.SKU[:15].ljust(15)}\t', end='')
|
||||||
|
|
||||||
|
manufacturer_part = supplier_part.manufacturer_part
|
||||||
|
|
||||||
|
if manufacturer_part:
|
||||||
|
if manufacturer_part.manufacturer:
|
||||||
|
supplier_part.manufacturer = manufacturer_part.manufacturer
|
||||||
|
|
||||||
|
if manufacturer_part.MPN:
|
||||||
|
supplier_part.MPN = manufacturer_part.MPN
|
||||||
|
|
||||||
|
supplier_part.save()
|
||||||
|
|
||||||
|
print(f'[SUCCESS: UPDATED MANUFACTURER INFO]')
|
||||||
|
else:
|
||||||
|
print(f'[IGNORED: NO MANUFACTURER PART]')
|
||||||
|
|
||||||
|
print(f'{"-"*10}\nDone\n')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('company', '0035_supplierpart_update_1'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Make new ManufacturerPart with SupplierPart "manufacturer" and "MPN"
|
||||||
|
# fields, then link it to the new SupplierPart "manufacturer_part" field
|
||||||
|
migrations.RunPython(supplierpart_make_manufacturer_parts, reverse_code=supplierpart_populate_manufacturer_info),
|
||||||
|
]
|
21
InvenTree/company/migrations/0037_supplierpart_update_3.py
Normal file
21
InvenTree/company/migrations/0037_supplierpart_update_3.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import InvenTree.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('company', '0036_supplierpart_update_2'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='supplierpart',
|
||||||
|
name='MPN',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='supplierpart',
|
||||||
|
name='manufacturer',
|
||||||
|
),
|
||||||
|
]
|
@ -11,7 +11,9 @@ import math
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
from django.db.models import Sum, Q, UniqueConstraint
|
from django.db.models import Sum, Q, UniqueConstraint
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@ -208,7 +210,7 @@ class Company(models.Model):
|
|||||||
@property
|
@property
|
||||||
def parts(self):
|
def parts(self):
|
||||||
""" Return SupplierPart objects which are supplied or manufactured by this company """
|
""" Return SupplierPart objects which are supplied or manufactured by this company """
|
||||||
return SupplierPart.objects.filter(Q(supplier=self.id) | Q(manufacturer=self.id))
|
return SupplierPart.objects.filter(Q(supplier=self.id) | Q(manufacturer_part__manufacturer=self.id))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def part_count(self):
|
def part_count(self):
|
||||||
@ -223,7 +225,7 @@ class Company(models.Model):
|
|||||||
def stock_items(self):
|
def stock_items(self):
|
||||||
""" Return a list of all stock items supplied or manufactured by this company """
|
""" Return a list of all stock items supplied or manufactured by this company """
|
||||||
stock = apps.get_model('stock', 'StockItem')
|
stock = apps.get_model('stock', 'StockItem')
|
||||||
return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer=self.id)).all()
|
return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer_part__manufacturer=self.id)).all()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stock_count(self):
|
def stock_count(self):
|
||||||
@ -284,19 +286,106 @@ class Contact(models.Model):
|
|||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
class SupplierPart(models.Model):
|
class ManufacturerPart(models.Model):
|
||||||
""" Represents a unique part as provided by a Supplier
|
""" Represents a unique part as provided by a Manufacturer
|
||||||
Each SupplierPart is identified by a MPN (Manufacturer Part Number)
|
Each ManufacturerPart is identified by a MPN (Manufacturer Part Number)
|
||||||
Each SupplierPart is also linked to a Part object.
|
Each ManufacturerPart is also linked to a Part object.
|
||||||
A Part may be available from multiple suppliers
|
A Part may be available from multiple manufacturers
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
part: Link to the master Part
|
part: Link to the master Part
|
||||||
|
manufacturer: Company that manufactures the ManufacturerPart
|
||||||
|
MPN: Manufacture part number
|
||||||
|
link: Link to external website for this manufacturer part
|
||||||
|
description: Descriptive notes field
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('part', 'manufacturer', 'MPN')
|
||||||
|
|
||||||
|
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||||
|
related_name='manufacturer_parts',
|
||||||
|
verbose_name=_('Base Part'),
|
||||||
|
limit_choices_to={
|
||||||
|
'purchaseable': True,
|
||||||
|
},
|
||||||
|
help_text=_('Select part'),
|
||||||
|
)
|
||||||
|
|
||||||
|
manufacturer = models.ForeignKey(
|
||||||
|
Company,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
related_name='manufactured_parts',
|
||||||
|
limit_choices_to={
|
||||||
|
'is_manufacturer': True
|
||||||
|
},
|
||||||
|
verbose_name=_('Manufacturer'),
|
||||||
|
help_text=_('Select manufacturer'),
|
||||||
|
)
|
||||||
|
|
||||||
|
MPN = models.CharField(
|
||||||
|
null=True,
|
||||||
|
max_length=100,
|
||||||
|
verbose_name=_('MPN'),
|
||||||
|
help_text=_('Manufacturer Part Number')
|
||||||
|
)
|
||||||
|
|
||||||
|
link = InvenTreeURLField(
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Link'),
|
||||||
|
help_text=_('URL for external manufacturer part link')
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=250, blank=True, null=True,
|
||||||
|
verbose_name=_('Description'),
|
||||||
|
help_text=_('Manufacturer part description')
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, part, manufacturer, mpn, description, link=None):
|
||||||
|
""" Check if ManufacturerPart instance does not already exist
|
||||||
|
then create it
|
||||||
|
"""
|
||||||
|
|
||||||
|
manufacturer_part = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
manufacturer_part = ManufacturerPart.objects.get(part=part, manufacturer=manufacturer, MPN=mpn)
|
||||||
|
except ManufacturerPart.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not manufacturer_part:
|
||||||
|
manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=mpn, description=description, link=link)
|
||||||
|
manufacturer_part.save()
|
||||||
|
|
||||||
|
return manufacturer_part
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
s = ''
|
||||||
|
|
||||||
|
if self.manufacturer:
|
||||||
|
s += f'{self.manufacturer.name}'
|
||||||
|
s += ' | '
|
||||||
|
|
||||||
|
s += f'{self.MPN}'
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPart(models.Model):
|
||||||
|
""" Represents a unique part as provided by a Supplier
|
||||||
|
Each SupplierPart is identified by a SKU (Supplier Part Number)
|
||||||
|
Each SupplierPart is also linked to a Part or ManufacturerPart object.
|
||||||
|
A Part may be available from multiple suppliers
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
part: Link to the master Part (Obsolete)
|
||||||
|
source_item: The sourcing item linked to this SupplierPart instance
|
||||||
supplier: Company that supplies this SupplierPart object
|
supplier: Company that supplies this SupplierPart object
|
||||||
SKU: Stock keeping unit (supplier part number)
|
SKU: Stock keeping unit (supplier part number)
|
||||||
manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!)
|
link: Link to external website for this supplier part
|
||||||
MPN: Manufacture part number
|
|
||||||
link: Link to external website for this part
|
|
||||||
description: Descriptive notes field
|
description: Descriptive notes field
|
||||||
note: Longer form note field
|
note: Longer form note field
|
||||||
base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee"
|
base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee"
|
||||||
@ -308,6 +397,57 @@ class SupplierPart(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
""" Overriding save method to process the linked ManufacturerPart
|
||||||
|
"""
|
||||||
|
|
||||||
|
if 'manufacturer' in kwargs:
|
||||||
|
manufacturer_id = kwargs.pop('manufacturer')
|
||||||
|
|
||||||
|
try:
|
||||||
|
manufacturer = Company.objects.get(pk=int(manufacturer_id))
|
||||||
|
except (ValueError, Company.DoesNotExist):
|
||||||
|
manufacturer = None
|
||||||
|
else:
|
||||||
|
manufacturer = None
|
||||||
|
if 'MPN' in kwargs:
|
||||||
|
MPN = kwargs.pop('MPN')
|
||||||
|
else:
|
||||||
|
MPN = None
|
||||||
|
|
||||||
|
if manufacturer or MPN:
|
||||||
|
if not self.manufacturer_part:
|
||||||
|
# Create ManufacturerPart
|
||||||
|
manufacturer_part = ManufacturerPart.create(part=self.part,
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
mpn=MPN,
|
||||||
|
description=self.description)
|
||||||
|
self.manufacturer_part = manufacturer_part
|
||||||
|
else:
|
||||||
|
# Update ManufacturerPart (if ID exists)
|
||||||
|
try:
|
||||||
|
manufacturer_part_id = self.manufacturer_part.id
|
||||||
|
except AttributeError:
|
||||||
|
manufacturer_part_id = None
|
||||||
|
|
||||||
|
if manufacturer_part_id:
|
||||||
|
try:
|
||||||
|
(manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part,
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
MPN=MPN)
|
||||||
|
except IntegrityError:
|
||||||
|
manufacturer_part = None
|
||||||
|
raise ValidationError(f'ManufacturerPart linked to {self.part} from manufacturer {manufacturer.name}'
|
||||||
|
f'with part number {MPN} already exists!')
|
||||||
|
|
||||||
|
if manufacturer_part:
|
||||||
|
self.manufacturer_part = manufacturer_part
|
||||||
|
|
||||||
|
self.clean()
|
||||||
|
self.validate_unique()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('part', 'supplier', 'SKU')
|
unique_together = ('part', 'supplier', 'SKU')
|
||||||
|
|
||||||
@ -336,23 +476,12 @@ class SupplierPart(models.Model):
|
|||||||
help_text=_('Supplier stock keeping unit')
|
help_text=_('Supplier stock keeping unit')
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer_part = models.ForeignKey(ManufacturerPart, on_delete=models.CASCADE,
|
||||||
Company,
|
blank=True, null=True,
|
||||||
on_delete=models.SET_NULL,
|
related_name='supplier_parts',
|
||||||
related_name='manufactured_parts',
|
verbose_name=_('Manufacturer Part'),
|
||||||
limit_choices_to={
|
help_text=_('Select manufacturer part'),
|
||||||
'is_manufacturer': True
|
)
|
||||||
},
|
|
||||||
verbose_name=_('Manufacturer'),
|
|
||||||
help_text=_('Select manufacturer'),
|
|
||||||
null=True, blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
MPN = models.CharField(
|
|
||||||
max_length=100, blank=True, null=True,
|
|
||||||
verbose_name=_('MPN'),
|
|
||||||
help_text=_('Manufacturer part number')
|
|
||||||
)
|
|
||||||
|
|
||||||
link = InvenTreeURLField(
|
link = InvenTreeURLField(
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
@ -389,10 +518,11 @@ class SupplierPart(models.Model):
|
|||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
if self.manufacturer:
|
if self.manufacturer_part:
|
||||||
items.append(self.manufacturer.name)
|
if self.manufacturer_part.manufacturer:
|
||||||
if self.MPN:
|
items.append(self.manufacturer_part.manufacturer.name)
|
||||||
items.append(self.MPN)
|
if self.manufacturer_part.MPN:
|
||||||
|
items.append(self.manufacturer_part.MPN)
|
||||||
|
|
||||||
return ' | '.join(items)
|
return ' | '.join(items)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from rest_framework import serializers
|
|||||||
from sql_util.utils import SubqueryCount
|
from sql_util.utils import SubqueryCount
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import ManufacturerPart
|
||||||
from .models import SupplierPart, SupplierPriceBreak
|
from .models import SupplierPart, SupplierPriceBreak
|
||||||
|
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
@ -80,6 +81,49 @@ class CompanySerializer(InvenTreeModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartSerializer(InvenTreeModelSerializer):
|
||||||
|
""" Serializer for ManufacturerPart object """
|
||||||
|
|
||||||
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
|
|
||||||
|
manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True)
|
||||||
|
|
||||||
|
pretty_name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
|
manufacturer_detail = kwargs.pop('manufacturer_detail', False)
|
||||||
|
prettify = kwargs.pop('pretty', False)
|
||||||
|
|
||||||
|
super(ManufacturerPartSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if part_detail is not True:
|
||||||
|
self.fields.pop('part_detail')
|
||||||
|
|
||||||
|
if manufacturer_detail is not True:
|
||||||
|
self.fields.pop('manufacturer_detail')
|
||||||
|
|
||||||
|
if prettify is not True:
|
||||||
|
self.fields.pop('pretty_name')
|
||||||
|
|
||||||
|
manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ManufacturerPart
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'part',
|
||||||
|
'part_detail',
|
||||||
|
'pretty_name',
|
||||||
|
'manufacturer',
|
||||||
|
'manufacturer_detail',
|
||||||
|
'description',
|
||||||
|
'MPN',
|
||||||
|
'link',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartSerializer(InvenTreeModelSerializer):
|
class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializer for SupplierPart object """
|
""" Serializer for SupplierPart object """
|
||||||
|
|
||||||
@ -87,7 +131,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
|
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
|
||||||
|
|
||||||
manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True)
|
manufacturer_detail = CompanyBriefSerializer(source='manufacturer_part.manufacturer', many=False, read_only=True)
|
||||||
|
|
||||||
pretty_name = serializers.CharField(read_only=True)
|
pretty_name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
@ -113,8 +157,12 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
self.fields.pop('pretty_name')
|
self.fields.pop('pretty_name')
|
||||||
|
|
||||||
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
|
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
|
||||||
|
|
||||||
|
manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True)
|
||||||
|
|
||||||
|
MPN = serializers.StringRelatedField(source='manufacturer_part.MPN')
|
||||||
|
|
||||||
manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True))
|
manufacturer_part = ManufacturerPartSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
@ -127,12 +175,31 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
'supplier_detail',
|
'supplier_detail',
|
||||||
'SKU',
|
'SKU',
|
||||||
'manufacturer',
|
'manufacturer',
|
||||||
'manufacturer_detail',
|
|
||||||
'description',
|
|
||||||
'MPN',
|
'MPN',
|
||||||
|
'manufacturer_detail',
|
||||||
|
'manufacturer_part',
|
||||||
|
'description',
|
||||||
'link',
|
'link',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
""" Extract manufacturer data and process ManufacturerPart """
|
||||||
|
|
||||||
|
# Create SupplierPart
|
||||||
|
supplier_part = super().create(validated_data)
|
||||||
|
|
||||||
|
# Get ManufacturerPart raw data (unvalidated)
|
||||||
|
manufacturer_id = self.initial_data.get('manufacturer', None)
|
||||||
|
MPN = self.initial_data.get('MPN', None)
|
||||||
|
|
||||||
|
if manufacturer_id or MPN:
|
||||||
|
kwargs = {'manufacturer': manufacturer_id,
|
||||||
|
'MPN': MPN,
|
||||||
|
}
|
||||||
|
supplier_part.save(**kwargs)
|
||||||
|
|
||||||
|
return supplier_part
|
||||||
|
|
||||||
|
|
||||||
class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
|
class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializer for SupplierPriceBreak object """
|
""" Serializer for SupplierPriceBreak object """
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Company" %} - {{ company.name }}
|
{% inventree_title %} | {% trans "Company" %} - {{ company.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,11 +21,13 @@
|
|||||||
<td>{% trans "Company Name" %}</td>
|
<td>{% trans "Company Name" %}</td>
|
||||||
<td>{{ company.name }}</td>
|
<td>{{ company.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if company.description %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Description" %}</td>
|
<td>{% trans "Description" %}</td>
|
||||||
<td>{{ company.description }}</td>
|
<td>{{ company.description }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-globe'></span></td>
|
<td><span class='fas fa-globe'></span></td>
|
||||||
<td>{% trans "Website" %}</td>
|
<td>{% trans "Website" %}</td>
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
{% extends "company/company_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include 'company/navbar.html' with tab='manufacturer_parts' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Manufacturer Parts" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
{% if roles.purchase_order.change %}
|
||||||
|
<div id='button-toolbar'>
|
||||||
|
<div class='button-toolbar container-fluid'>
|
||||||
|
<div class='btn-group role='group'>
|
||||||
|
{% if roles.purchase_order.add %}
|
||||||
|
<button class="btn btn-success" id='manufacturer-part-create' title='{% trans "Create new manufacturer part" %}'>
|
||||||
|
<span class='fas fa-plus-circle'></span> {% trans "New Manufacturer Part" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<div class='btn-group'>
|
||||||
|
<div class="dropdown" style="float: right;">
|
||||||
|
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% if roles.purchase_order.add %}
|
||||||
|
<li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if roles.purchase_order.delete %}
|
||||||
|
<li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='filter-list' id='filter-list-supplier-part'>
|
||||||
|
<!-- Empty div (will be filled out with available BOM filters) -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<table class='table table-striped table-condensed' id='part-table' data-toolbar='#button-toolbar'>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
$("#manufacturer-part-create").click(function () {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'manufacturer-part-create' %}",
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
manufacturer: {{ company.id }},
|
||||||
|
},
|
||||||
|
reload: true,
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
field: 'part',
|
||||||
|
label: '{% trans "New Part" %}',
|
||||||
|
title: '{% trans "Create new Part" %}',
|
||||||
|
url: "{% url 'part-create' %}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'manufacturer',
|
||||||
|
label: '{% trans "New Manufacturer" %}',
|
||||||
|
title: '{% trans "Create new Manufacturer" %}',
|
||||||
|
url: "{% url 'manufacturer-create' %}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
loadManufacturerPartTable(
|
||||||
|
"#part-table",
|
||||||
|
"{% url 'api-manufacturer-part-list' %}",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
part_detail: true,
|
||||||
|
manufacturer_detail: true,
|
||||||
|
company: {{ company.id }},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$("#multi-part-delete").click(function() {
|
||||||
|
var selections = $("#part-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
var parts = [];
|
||||||
|
|
||||||
|
selections.forEach(function(item) {
|
||||||
|
parts.push(item.pk);
|
||||||
|
});
|
||||||
|
|
||||||
|
var url = "{% url 'manufacturer-part-delete' %}"
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
data: {
|
||||||
|
parts: parts,
|
||||||
|
},
|
||||||
|
reload: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#multi-part-order").click(function() {
|
||||||
|
var selections = $("#part-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
var parts = [];
|
||||||
|
|
||||||
|
selections.forEach(function(item) {
|
||||||
|
parts.push(item.part);
|
||||||
|
});
|
||||||
|
|
||||||
|
launchModalForm("/order/purchase-order/order-parts/", {
|
||||||
|
data: {
|
||||||
|
parts: parts,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -1,9 +1,10 @@
|
|||||||
{% extends "company/company_base.html" %}
|
{% extends "company/company_base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block menubar %}
|
{% block menubar %}
|
||||||
{% include 'company/navbar.html' with tab='parts' %}
|
{% include 'company/navbar.html' with tab='supplier_parts' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
@ -17,9 +18,9 @@
|
|||||||
<div class='button-toolbar container-fluid'>
|
<div class='button-toolbar container-fluid'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
{% if roles.purchase_order.add %}
|
{% if roles.purchase_order.add %}
|
||||||
<button class="btn btn-success" id='part-create' title='{% trans "Create new supplier part" %}'>
|
<button class="btn btn-success" id='supplier-part-create' title='{% trans "Create new supplier part" %}'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<div class="dropdown" style="float: right;">
|
<div class="dropdown" style="float: right;">
|
||||||
@ -51,13 +52,12 @@
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$("#part-create").click(function () {
|
$("#supplier-part-create").click(function () {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'supplier-part-create' %}",
|
"{% url 'supplier-part-create' %}",
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
{% if company.is_supplier %}supplier: {{ company.id }},{% endif %}
|
supplier: {{ company.id }},
|
||||||
{% if company.is_manufacturer %}manufacturer: {{ company.id }},{% endif %}
|
|
||||||
},
|
},
|
||||||
reload: true,
|
reload: true,
|
||||||
secondary: [
|
secondary: [
|
||||||
@ -73,12 +73,6 @@
|
|||||||
title: "{% trans 'Create new Supplier' %}",
|
title: "{% trans 'Create new Supplier' %}",
|
||||||
url: "{% url 'supplier-create' %}",
|
url: "{% url 'supplier-create' %}",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
field: 'manufacturer',
|
|
||||||
label: '{% trans "New Manufacturer" %}',
|
|
||||||
title: '{% trans "Create new Manufacturer" %}',
|
|
||||||
url: "{% url 'manufacturer-create' %}",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -105,7 +99,9 @@
|
|||||||
parts.push(item.pk);
|
parts.push(item.pk);
|
||||||
});
|
});
|
||||||
|
|
||||||
launchModalForm("{% url 'supplier-part-delete' %}", {
|
var url = "{% url 'supplier-part-delete' %}"
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
data: {
|
data: {
|
||||||
parts: parts,
|
parts: parts,
|
||||||
},
|
},
|
@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Supplier List" %}
|
{% inventree_title %} | {% trans "Supplier List" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
133
InvenTree/company/templates/company/manufacturer_part_base.html
Normal file
133
InvenTree/company/templates/company/manufacturer_part_base.html
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
{% extends "two_column.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block page_title %}
|
||||||
|
InvenTree | {% trans "Manufacturer Part" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block thumbnail %}
|
||||||
|
<img class='part-thumb'
|
||||||
|
{% if part.part.image %}
|
||||||
|
src='{{ part.part.image.url }}'
|
||||||
|
{% else %}
|
||||||
|
src="{% static 'img/blank_image.png' %}"
|
||||||
|
{% endif %}/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_data %}
|
||||||
|
<h3>{% trans "Manufacturer Part" %}</h3>
|
||||||
|
<hr>
|
||||||
|
<h4>
|
||||||
|
{{ part.part.full_name }}
|
||||||
|
{% if user.is_staff and perms.company.change_company %}
|
||||||
|
<a href="{% url 'admin:company_supplierpart_change' part.pk %}">
|
||||||
|
<span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
<p>{{ part.manufacturer.name }} - {{ part.MPN }}</p>
|
||||||
|
|
||||||
|
{% if roles.purchase_order.change %}
|
||||||
|
<div class='btn-row'>
|
||||||
|
<div class='btn-group action-buttons' role='group'>
|
||||||
|
{% comment "for later" %}
|
||||||
|
{% if roles.purchase_order.add %}
|
||||||
|
<button type='button' class='btn btn-default btn-glyph' id='order-part' title='{% trans "Order part" %}'>
|
||||||
|
<span class='fas fa-shopping-cart'></span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endcomment %}
|
||||||
|
<button type='button' class='btn btn-default btn-glyph' id='edit-part' title='{% trans "Edit manufacturer part" %}'>
|
||||||
|
<span class='fas fa-edit icon-green'/>
|
||||||
|
</button>
|
||||||
|
{% if roles.purchase_order.delete %}
|
||||||
|
<button type='button' class='btn btn-default btn-glyph' id='delete-part' title='{% trans "Delete manufacturer part" %}'>
|
||||||
|
<span class='fas fa-trash-alt icon-red'/>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_details %}
|
||||||
|
|
||||||
|
<h4>{% trans "Manufacturer Part Details" %}</h4>
|
||||||
|
<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-manufacturers' part.part.id %}">{{ part.part.full_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if part.description %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{% trans "Description" %}</td>
|
||||||
|
<td>{{ part.description }}</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></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-industry'></span></td>
|
||||||
|
<td>{% trans "Manufacturer" %}</td>
|
||||||
|
<td><a href="{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a></td></tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
|
<td>{% trans "MPN" %}</td>
|
||||||
|
<td>{{ part.MPN }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
enableNavbar({
|
||||||
|
label: 'manufacturer-part',
|
||||||
|
toggleId: '#manufacturer-part-menu-toggle'
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#order-part, #order-part2').click(function() {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'order-parts' %}",
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
part: {{ part.part.id }},
|
||||||
|
},
|
||||||
|
reload: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#edit-part').click(function () {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'manufacturer-part-edit' part.id %}",
|
||||||
|
{
|
||||||
|
reload: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#delete-part').click(function() {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'manufacturer-part-delete' %}?part={{ part.id }}",
|
||||||
|
{
|
||||||
|
redirect: "{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
{% if part %}
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% include "hover_image.html" with image=part.image %}
|
||||||
|
{{ part.full_name}}
|
||||||
|
<br>
|
||||||
|
<i>{{ part.description }}</i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,46 @@
|
|||||||
|
{% extends "modal_delete_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
<div class='alert alert-block alert-warning'>
|
||||||
|
{% trans "Are you sure you want to delete the following Manufacturer Parts?" %}
|
||||||
|
</div>
|
||||||
|
{% for part in parts %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form_data %}
|
||||||
|
|
||||||
|
{% for part in parts %}
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
<tr>
|
||||||
|
<input type='hidden' name='manufacturer-part-{{ part.id}}' value='manufacturer-part-{{ part.id }}'/>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% include "hover_image.html" with image=part.part.image %}
|
||||||
|
{{ part.part.full_name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% include "hover_image.html" with image=part.manufacturer.image %}
|
||||||
|
{{ part.manufacturer.name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ part.MPN }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% if part.supplier_parts.all|length > 0 %}
|
||||||
|
<div class='alert alert-block alert-danger'>
|
||||||
|
<p>{% blocktrans with count=part.supplier_parts.all|length %}There are {{count}} suppliers defined for this manufacturer part. If you delete it, the following supplier parts will also be deleted:{% endblocktrans %}</p>
|
||||||
|
<ul class='list-group' style='margin-top:10px'>
|
||||||
|
{% for spart in part.supplier_parts.all %}
|
||||||
|
<li class='list-group-item'>{{ spart.supplier.name }} - {{ spart.SKU }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,38 @@
|
|||||||
|
{% extends "company/manufacturer_part_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include "company/manufacturer_part_navbar.html" with tab='details' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Manufacturer Part Details" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
<table class="table table-striped table-condensed">
|
||||||
|
<tr>
|
||||||
|
<td>{% trans "Internal Part" %}</td>
|
||||||
|
<td>
|
||||||
|
{% if part.part %}
|
||||||
|
<a href="{% url 'part-manufacturers' part.part.id %}">{{ part.part.full_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td>{% trans "Manufacturer" %}</td><td><a href="{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a></td></tr>
|
||||||
|
<tr><td>{% trans "MPN" %}</td><td>{{ part.MPN }}</tr></tr>
|
||||||
|
{% if part.link %}
|
||||||
|
<tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,34 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<ul class='list-group'>
|
||||||
|
|
||||||
|
<li class='list-group-item'>
|
||||||
|
<a href='#' id='manufacturer-part-menu-toggle'>
|
||||||
|
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item {% if tab == "suppliers" %}active{% endif %}' title='{% trans "Supplier Parts" %}'>
|
||||||
|
<a href='{% url "manufacturer-part-suppliers" part.id %}'>
|
||||||
|
<span class='fas fa-building'></span>
|
||||||
|
{% trans "Suppliers" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% comment "for later" %}
|
||||||
|
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Manufacturer Part Stock" %}'>
|
||||||
|
<a href='{% url "manufacturer-part-stock" part.id %}'>
|
||||||
|
<span class='fas fa-boxes'></span>
|
||||||
|
{% trans "Stock" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item {% if tab == "orders" %}active{% endif %}' title='{% trans "Manufacturer Part Orders" %}'>
|
||||||
|
<a href='{% url "manufacturer-part-orders" part.id %}'>
|
||||||
|
<span class='fas fa-shopping-cart'></span>
|
||||||
|
{% trans "Orders" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
</ul>
|
@ -0,0 +1,89 @@
|
|||||||
|
{% extends "company/manufacturer_part_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include "company/manufacturer_part_navbar.html" with tab='suppliers' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Supplier Parts" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
<div id='button-toolbar'>
|
||||||
|
<div class='btn-group'>
|
||||||
|
<button class="btn btn-success" id='supplier-create'>
|
||||||
|
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
||||||
|
</button>
|
||||||
|
<div id='opt-dropdown' class="btn-group">
|
||||||
|
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'>{% trans "Delete" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
$('#supplier-create').click(function () {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'supplier-part-create' %}",
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
data: {
|
||||||
|
manufacturer_part: {{ part.id }}
|
||||||
|
},
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
field: 'supplier',
|
||||||
|
label: '{% trans "New Supplier" %}',
|
||||||
|
title: '{% trans "Create new supplier" %}',
|
||||||
|
url: "{% url 'supplier-create' %}"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#supplier-part-delete").click(function() {
|
||||||
|
|
||||||
|
var selections = $("#supplier-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
var parts = [];
|
||||||
|
|
||||||
|
selections.forEach(function(item) {
|
||||||
|
parts.push(item.pk);
|
||||||
|
});
|
||||||
|
|
||||||
|
launchModalForm("{% url 'supplier-part-delete' %}", {
|
||||||
|
data: {
|
||||||
|
parts: parts,
|
||||||
|
},
|
||||||
|
reload: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
loadSupplierPartTable(
|
||||||
|
"#supplier-table",
|
||||||
|
"{% url 'api-supplier-part-list' %}",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
part: {{ part.part.id }},
|
||||||
|
manufacturer_part: {{ part.id }},
|
||||||
|
part_detail: false,
|
||||||
|
supplier_detail: true,
|
||||||
|
manufacturer_detail: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -16,14 +16,25 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if company.is_supplier or company.is_manufacturer %}
|
{% if company.is_manufacturer %}
|
||||||
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Supplied Parts" %}'>
|
<li class='list-group-item {% if tab == "manufacturer_parts" %}active{% endif %}' title='{% trans "Manufactured Parts" %}'>
|
||||||
<a href='{% url "company-detail-parts" company.id %}'>
|
<a href='{% url "company-detail-manufacturer-parts" company.id %}'>
|
||||||
<span class='fas fa-shapes'></span>
|
<span class='fas fa-industry'></span>
|
||||||
{% trans "Parts" %}
|
{% trans "Manufactured Parts" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if company.is_supplier or company.is_manufacturer %}
|
||||||
|
<li class='list-group-item {% if tab == "supplier_parts" %}active{% endif %}' title='{% trans "Supplied Parts" %}'>
|
||||||
|
<a href='{% url "company-detail-supplier-parts" company.id %}'>
|
||||||
|
<span class='fas fa-building'></span>
|
||||||
|
{% trans "Supplied Parts" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if company.is_manufacturer or company.is_supplier %}
|
||||||
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Stock Items" %}'>
|
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Stock Items" %}'>
|
||||||
<a href='{% url "company-detail-stock" company.id %}'>
|
<a href='{% url "company-detail-stock" company.id %}'>
|
||||||
<span class='fas fa-boxes'></span>
|
<span class='fas fa-boxes'></span>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
{% extends "two_column.html" %}
|
{% extends "two_column.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Supplier Part" %}
|
{% inventree_title %} | {% trans "Supplier Part" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
@ -81,23 +82,24 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-building'></span></td>
|
<td><span class='fas fa-building'></span></td>
|
||||||
<td>{% trans "Supplier" %}</td>
|
<td>{% trans "Supplier" %}</td>
|
||||||
<td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
|
<td><a href="{% url 'company-detail-supplier-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
<td>{% trans "SKU" %}</td>
|
<td>{% trans "SKU" %}</td>
|
||||||
<td>{{ part.SKU }}</tr>
|
<td>{{ part.SKU }}</tr>
|
||||||
</tr>
|
</tr>
|
||||||
{% if part.manufacturer %}
|
{% if part.manufacturer_part.manufacturer %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-industry'></span></td>
|
<td><span class='fas fa-industry'></span></td>
|
||||||
<td>{% trans "Manufacturer" %}</td>
|
<td>{% trans "Manufacturer" %}</td>
|
||||||
<td><a href="{% url 'company-detail-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a></td></tr>
|
<td><a href="{% url 'company-detail-manufacturer-parts' part.manufacturer_part.manufacturer.id %}">{{ part.manufacturer_part.manufacturer.name }}</a></td>
|
||||||
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.MPN %}
|
{% if part.manufacturer_part.MPN %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
<td>{% trans "MPN" %}</td>
|
<td>{% trans "MPN" %}</td>
|
||||||
<td>{{ part.MPN }}</td>
|
<td><a href="{% url 'manufacturer-part-detail' part.manufacturer_part.id %}">{{ part.manufacturer_part.MPN }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.packaging %}
|
{% if part.packaging %}
|
||||||
@ -150,7 +152,7 @@ $('#delete-part').click(function() {
|
|||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'supplier-part-delete' %}?part={{ part.id }}",
|
"{% url 'supplier-part-delete' %}?part={{ part.id }}",
|
||||||
{
|
{
|
||||||
redirect: "{% url 'company-detail-parts' part.supplier.id %}"
|
redirect: "{% url 'company-detail-supplier-parts' part.supplier.id %}"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -13,13 +13,16 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
|
<input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% include "hover_image.html" with image=part.part.image %}
|
||||||
|
{{ part.part.full_name }}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% include "hover_image.html" with image=part.supplier.image %}
|
{% include "hover_image.html" with image=part.supplier.image %}
|
||||||
{{ part.supplier.name }}
|
{{ part.supplier.name }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% include "hover_image.html" with image=part.part.image %}
|
{{ part.SKU }}
|
||||||
{{ part.part.full_name }}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
@ -3,7 +3,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block menubar %}
|
{% block menubar %}
|
||||||
{% include "company/part_navbar.html" with tab='details' %}
|
{% include "company/supplier_part_navbar.html" with tab='details' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
@ -22,7 +22,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr><td>{% trans "Supplier" %}</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
|
<tr><td>{% trans "Supplier" %}</td><td><a href="{% url 'company-detail-supplier-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
|
||||||
<tr><td>{% trans "SKU" %}</td><td>{{ part.SKU }}</tr></tr>
|
<tr><td>{% trans "SKU" %}</td><td>{{ part.SKU }}</tr></tr>
|
||||||
{% if part.link %}
|
{% if part.link %}
|
||||||
<tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr>
|
<tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
<ul class='list-group'>
|
<ul class='list-group'>
|
||||||
|
|
@ -3,7 +3,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block menubar %}
|
{% block menubar %}
|
||||||
{% include "company/part_navbar.html" with tab='orders' %}
|
{% include "company/supplier_part_navbar.html" with tab='orders' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block menubar %}
|
{% block menubar %}
|
||||||
{% include "company/part_navbar.html" with tab='pricing' %}
|
{% include "company/supplier_part_navbar.html" with tab='pricing' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block menubar %}
|
{% block menubar %}
|
||||||
{% include "company/part_navbar.html" with tab='stock' %}
|
{% include "company/supplier_part_navbar.html" with tab='stock' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
|
@ -27,7 +27,7 @@ class CompanyTest(InvenTreeAPITestCase):
|
|||||||
def test_company_list(self):
|
def test_company_list(self):
|
||||||
url = reverse('api-company-list')
|
url = reverse('api-company-list')
|
||||||
|
|
||||||
# There should be two companies
|
# There should be three companies
|
||||||
response = self.get(url)
|
response = self.get(url)
|
||||||
self.assertEqual(len(response.data), 3)
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
@ -62,3 +62,90 @@ class CompanyTest(InvenTreeAPITestCase):
|
|||||||
data = {'search': 'cup'}
|
data = {'search': 'cup'}
|
||||||
response = self.get(url, data)
|
response = self.get(url, data)
|
||||||
self.assertEqual(len(response.data), 2)
|
self.assertEqual(len(response.data), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerTest(InvenTreeAPITestCase):
|
||||||
|
"""
|
||||||
|
Series of tests for the Manufacturer DRF API
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'location',
|
||||||
|
'company',
|
||||||
|
'manufacturer_part',
|
||||||
|
]
|
||||||
|
|
||||||
|
roles = [
|
||||||
|
'part.add',
|
||||||
|
'part.change',
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_manufacturer_part_list(self):
|
||||||
|
url = reverse('api-manufacturer-part-list')
|
||||||
|
|
||||||
|
# There should be three manufacturer parts
|
||||||
|
response = self.get(url)
|
||||||
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
# Create manufacturer part
|
||||||
|
data = {
|
||||||
|
'part': 1,
|
||||||
|
'manufacturer': 7,
|
||||||
|
'MPN': 'MPN_TEST',
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['MPN'], 'MPN_TEST')
|
||||||
|
|
||||||
|
# Filter by manufacturer
|
||||||
|
data = {'company': 7}
|
||||||
|
response = self.get(url, data)
|
||||||
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
# Filter by part
|
||||||
|
data = {'part': 5}
|
||||||
|
response = self.get(url, data)
|
||||||
|
self.assertEqual(len(response.data), 2)
|
||||||
|
|
||||||
|
def test_manufacturer_part_detail(self):
|
||||||
|
url = reverse('api-manufacturer-part-detail', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
response = self.get(url)
|
||||||
|
self.assertEqual(response.data['MPN'], 'MPN123')
|
||||||
|
|
||||||
|
# Change the MPN
|
||||||
|
data = {
|
||||||
|
'MPN': 'MPN-TEST-123',
|
||||||
|
}
|
||||||
|
response = self.client.patch(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['MPN'], 'MPN-TEST-123')
|
||||||
|
|
||||||
|
def test_manufacturer_part_search(self):
|
||||||
|
# Test search functionality in manufacturer list
|
||||||
|
url = reverse('api-manufacturer-part-list')
|
||||||
|
data = {'search': 'MPN'}
|
||||||
|
response = self.get(url, data)
|
||||||
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
def test_supplier_part_create(self):
|
||||||
|
url = reverse('api-supplier-part-list')
|
||||||
|
|
||||||
|
# Create supplier part
|
||||||
|
data = {
|
||||||
|
'part': 1,
|
||||||
|
'supplier': 1,
|
||||||
|
'SKU': 'SKU_TEST',
|
||||||
|
'manufacturer': 7,
|
||||||
|
'MPN': 'PART_NUMBER',
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
# Check manufacturer part
|
||||||
|
manufacturer_part_id = int(response.data['manufacturer_part']['pk'])
|
||||||
|
url = reverse('api-manufacturer-part-detail', kwargs={'pk': manufacturer_part_id})
|
||||||
|
response = self.get(url)
|
||||||
|
self.assertEqual(response.data['MPN'], 'PART_NUMBER')
|
||||||
|
@ -79,7 +79,7 @@ class TestManufacturerField(MigratorTestCase):
|
|||||||
part=part,
|
part=part,
|
||||||
supplier=supplier,
|
supplier=supplier,
|
||||||
SKU='SCREW.002',
|
SKU='SCREW.002',
|
||||||
manufacturer_name='Zero Corp'
|
manufacturer_name='Zero Corp',
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(Company.objects.count(), 1)
|
self.assertEqual(Company.objects.count(), 1)
|
||||||
@ -107,6 +107,136 @@ class TestManufacturerField(MigratorTestCase):
|
|||||||
self.assertEqual(part.manufacturer.name, 'ACME')
|
self.assertEqual(part.manufacturer.name, 'ACME')
|
||||||
|
|
||||||
|
|
||||||
|
class TestManufacturerPart(MigratorTestCase):
|
||||||
|
"""
|
||||||
|
Tests for migration 0034-0037 which added and transitioned to the ManufacturerPart model
|
||||||
|
"""
|
||||||
|
|
||||||
|
migrate_from = ('company', '0033_auto_20210410_1528')
|
||||||
|
migrate_to = ('company', '0037_supplierpart_update_3')
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
"""
|
||||||
|
Prepare the database by adding some test data 'before' the change:
|
||||||
|
|
||||||
|
- Part object
|
||||||
|
- Company object (supplier)
|
||||||
|
- SupplierPart object
|
||||||
|
"""
|
||||||
|
|
||||||
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
|
Company = self.old_state.apps.get_model('company', 'company')
|
||||||
|
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
|
||||||
|
|
||||||
|
# Create an initial part
|
||||||
|
part = Part.objects.create(
|
||||||
|
name='CAP CER 0.1UF 10V X5R 0402',
|
||||||
|
description='CAP CER 0.1UF 10V X5R 0402',
|
||||||
|
purchaseable=True,
|
||||||
|
level=0,
|
||||||
|
tree_id=0,
|
||||||
|
lft=0,
|
||||||
|
rght=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a manufacturer
|
||||||
|
manufacturer = Company.objects.create(
|
||||||
|
name='Murata',
|
||||||
|
description='Makes capacitors',
|
||||||
|
is_manufacturer=True,
|
||||||
|
is_supplier=False,
|
||||||
|
is_customer=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create suppliers
|
||||||
|
supplier_1 = Company.objects.create(
|
||||||
|
name='Digi-Key',
|
||||||
|
description='A supplier of components',
|
||||||
|
is_manufacturer=False,
|
||||||
|
is_supplier=True,
|
||||||
|
is_customer=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
supplier_2 = Company.objects.create(
|
||||||
|
name='Mouser',
|
||||||
|
description='We sell components',
|
||||||
|
is_manufacturer=False,
|
||||||
|
is_supplier=True,
|
||||||
|
is_customer=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add some SupplierPart objects
|
||||||
|
SupplierPart.objects.create(
|
||||||
|
part=part,
|
||||||
|
supplier=supplier_1,
|
||||||
|
SKU='DK-MUR-CAP-123456-ND',
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
MPN='MUR-CAP-123456',
|
||||||
|
)
|
||||||
|
|
||||||
|
SupplierPart.objects.create(
|
||||||
|
part=part,
|
||||||
|
supplier=supplier_1,
|
||||||
|
SKU='DK-MUR-CAP-987654-ND',
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
MPN='MUR-CAP-987654',
|
||||||
|
)
|
||||||
|
|
||||||
|
SupplierPart.objects.create(
|
||||||
|
part=part,
|
||||||
|
supplier=supplier_2,
|
||||||
|
SKU='CAP-CER-01UF',
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
MPN='MUR-CAP-123456',
|
||||||
|
)
|
||||||
|
|
||||||
|
# No MPN
|
||||||
|
SupplierPart.objects.create(
|
||||||
|
part=part,
|
||||||
|
supplier=supplier_2,
|
||||||
|
SKU='CAP-CER-01UF-1',
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# No Manufacturer
|
||||||
|
SupplierPart.objects.create(
|
||||||
|
part=part,
|
||||||
|
supplier=supplier_2,
|
||||||
|
SKU='CAP-CER-01UF-2',
|
||||||
|
MPN='MUR-CAP-123456',
|
||||||
|
)
|
||||||
|
|
||||||
|
# No Manufacturer data
|
||||||
|
SupplierPart.objects.create(
|
||||||
|
part=part,
|
||||||
|
supplier=supplier_2,
|
||||||
|
SKU='CAP-CER-01UF-3',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manufacturer_part_objects(self):
|
||||||
|
"""
|
||||||
|
Test that the new companies have been created successfully
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check on the SupplierPart objects
|
||||||
|
SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
|
||||||
|
|
||||||
|
supplier_parts = SupplierPart.objects.all()
|
||||||
|
self.assertEqual(supplier_parts.count(), 6)
|
||||||
|
|
||||||
|
supplier_parts = SupplierPart.objects.filter(supplier__name='Mouser')
|
||||||
|
self.assertEqual(supplier_parts.count(), 4)
|
||||||
|
|
||||||
|
# Check on the ManufacturerPart objects
|
||||||
|
ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart')
|
||||||
|
|
||||||
|
manufacturer_parts = ManufacturerPart.objects.all()
|
||||||
|
self.assertEqual(manufacturer_parts.count(), 4)
|
||||||
|
|
||||||
|
manufacturer_part = manufacturer_parts.first()
|
||||||
|
self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456')
|
||||||
|
|
||||||
|
|
||||||
class TestCurrencyMigration(MigratorTestCase):
|
class TestCurrencyMigration(MigratorTestCase):
|
||||||
"""
|
"""
|
||||||
Tests for upgrade from basic currency support to django-money
|
Tests for upgrade from basic currency support to django-money
|
||||||
|
@ -10,6 +10,7 @@ from django.urls import reverse
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
from .models import ManufacturerPart
|
||||||
from .models import SupplierPart
|
from .models import SupplierPart
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ class CompanyViewTestBase(TestCase):
|
|||||||
'part',
|
'part',
|
||||||
'location',
|
'location',
|
||||||
'company',
|
'company',
|
||||||
|
'manufacturer_part',
|
||||||
'supplier_part',
|
'supplier_part',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -200,3 +202,105 @@ class CompanyViewTest(CompanyViewTestBase):
|
|||||||
|
|
||||||
response = self.client.get(reverse('customer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.get(reverse('customer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
self.assertContains(response, 'Create new Customer')
|
self.assertContains(response, 'Create new Customer')
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartViewTests(CompanyViewTestBase):
|
||||||
|
"""
|
||||||
|
Tests for the ManufacturerPart views.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_manufacturer_part_create(self):
|
||||||
|
"""
|
||||||
|
Test the ManufacturerPartCreate view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = reverse('manufacturer-part-create')
|
||||||
|
|
||||||
|
# First check that we can GET the form
|
||||||
|
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# How many manufaturer parts are already in the database?
|
||||||
|
n = ManufacturerPart.objects.all().count()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'part': 1,
|
||||||
|
'manufacturer': 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
# MPN is required! (form should fail)
|
||||||
|
(response, errors) = self.post(url, data, valid=False)
|
||||||
|
|
||||||
|
self.assertIsNotNone(errors.get('MPN', None))
|
||||||
|
|
||||||
|
data['MPN'] = 'TEST-ME-123'
|
||||||
|
|
||||||
|
(response, errors) = self.post(url, data, valid=True)
|
||||||
|
|
||||||
|
# Check that the ManufacturerPart was created!
|
||||||
|
self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
|
||||||
|
|
||||||
|
# Try to create duplicate ManufacturerPart
|
||||||
|
(response, errors) = self.post(url, data, valid=False)
|
||||||
|
|
||||||
|
self.assertIsNotNone(errors.get('__all__', None))
|
||||||
|
|
||||||
|
# Check that the ManufacturerPart count stayed the same
|
||||||
|
self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
|
||||||
|
|
||||||
|
def test_supplier_part_create(self):
|
||||||
|
"""
|
||||||
|
Test that the SupplierPartCreate view creates Manufacturer Part.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = reverse('supplier-part-create')
|
||||||
|
|
||||||
|
# First check that we can GET the form
|
||||||
|
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# How many manufacturer parts are already in the database?
|
||||||
|
n = ManufacturerPart.objects.all().count()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'part': 1,
|
||||||
|
'supplier': 1,
|
||||||
|
'SKU': 'SKU_TEST',
|
||||||
|
'manufacturer': 6,
|
||||||
|
'MPN': 'MPN_TEST',
|
||||||
|
}
|
||||||
|
|
||||||
|
(response, errors) = self.post(url, data, valid=True)
|
||||||
|
|
||||||
|
# Check that the ManufacturerPart was created!
|
||||||
|
self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
|
||||||
|
|
||||||
|
def test_manufacturer_part_delete(self):
|
||||||
|
"""
|
||||||
|
Test the ManufacturerPartDelete view
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = reverse('manufacturer-part-delete')
|
||||||
|
|
||||||
|
# Get form using 'part' argument
|
||||||
|
response = self.client.get(url, {'part': '2'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# POST to delete manufacturer part
|
||||||
|
n = ManufacturerPart.objects.count()
|
||||||
|
m = SupplierPart.objects.count()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'manufacturer-part-2': 'manufacturer-part-2',
|
||||||
|
'confirm_delete': True
|
||||||
|
},
|
||||||
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Check that the ManufacturerPart was deleted
|
||||||
|
self.assertEqual(n - 1, ManufacturerPart.objects.count())
|
||||||
|
# Check that the SupplierParts were deleted
|
||||||
|
self.assertEqual(m - 2, SupplierPart.objects.count())
|
||||||
|
@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .models import Company, Contact, SupplierPart
|
from .models import Company, Contact, ManufacturerPart, SupplierPart
|
||||||
from .models import rename_company_image
|
from .models import rename_company_image
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ class CompanySimpleTest(TestCase):
|
|||||||
'part',
|
'part',
|
||||||
'location',
|
'location',
|
||||||
'bom',
|
'bom',
|
||||||
|
'manufacturer_part',
|
||||||
'supplier_part',
|
'supplier_part',
|
||||||
'price_breaks',
|
'price_breaks',
|
||||||
]
|
]
|
||||||
@ -74,10 +75,10 @@ class CompanySimpleTest(TestCase):
|
|||||||
self.assertEqual(acme.supplied_part_count, 4)
|
self.assertEqual(acme.supplied_part_count, 4)
|
||||||
|
|
||||||
self.assertTrue(appel.has_parts)
|
self.assertTrue(appel.has_parts)
|
||||||
self.assertEqual(appel.supplied_part_count, 2)
|
self.assertEqual(appel.supplied_part_count, 3)
|
||||||
|
|
||||||
self.assertTrue(zerg.has_parts)
|
self.assertTrue(zerg.has_parts)
|
||||||
self.assertEqual(zerg.supplied_part_count, 1)
|
self.assertEqual(zerg.supplied_part_count, 2)
|
||||||
|
|
||||||
def test_price_breaks(self):
|
def test_price_breaks(self):
|
||||||
|
|
||||||
@ -166,3 +167,53 @@ class ContactSimpleTest(TestCase):
|
|||||||
# Remove the parent company
|
# Remove the parent company
|
||||||
Company.objects.get(pk=self.c.pk).delete()
|
Company.objects.get(pk=self.c.pk).delete()
|
||||||
self.assertEqual(Contact.objects.count(), 0)
|
self.assertEqual(Contact.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartSimpleTest(TestCase):
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'company',
|
||||||
|
'location',
|
||||||
|
'part',
|
||||||
|
'manufacturer_part',
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Create a manufacturer part
|
||||||
|
self.part = Part.objects.get(pk=1)
|
||||||
|
manufacturer = Company.objects.get(pk=1)
|
||||||
|
|
||||||
|
self.mp = ManufacturerPart.create(
|
||||||
|
part=self.part,
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
mpn='PART_NUMBER',
|
||||||
|
description='THIS IS A MANUFACTURER PART',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a supplier part
|
||||||
|
supplier = Company.objects.get(pk=5)
|
||||||
|
supplier_part = SupplierPart.objects.create(
|
||||||
|
part=self.part,
|
||||||
|
supplier=supplier,
|
||||||
|
SKU='SKU_TEST',
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'manufacturer': manufacturer.id,
|
||||||
|
'MPN': 'MPN_TEST',
|
||||||
|
}
|
||||||
|
supplier_part.save(**kwargs)
|
||||||
|
|
||||||
|
def test_exists(self):
|
||||||
|
self.assertEqual(ManufacturerPart.objects.count(), 5)
|
||||||
|
|
||||||
|
# Check that manufacturer part was created from supplier part creation
|
||||||
|
manufacturer_parts = ManufacturerPart.objects.filter(manufacturer=1)
|
||||||
|
self.assertEqual(manufacturer_parts.count(), 2)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
# Remove a part
|
||||||
|
Part.objects.get(pk=self.part.id).delete()
|
||||||
|
# Check that ManufacturerPart was deleted
|
||||||
|
self.assertEqual(ManufacturerPart.objects.count(), 3)
|
||||||
|
@ -13,7 +13,8 @@ company_detail_urls = [
|
|||||||
|
|
||||||
# url(r'orders/?', views.CompanyDetail.as_view(template_name='company/orders.html'), name='company-detail-orders'),
|
# url(r'orders/?', views.CompanyDetail.as_view(template_name='company/orders.html'), name='company-detail-orders'),
|
||||||
|
|
||||||
url(r'^parts/', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'),
|
url(r'^supplier-parts/', views.CompanyDetail.as_view(template_name='company/detail_supplier_part.html'), name='company-detail-supplier-parts'),
|
||||||
|
url(r'^manufacturer-parts/', views.CompanyDetail.as_view(template_name='company/detail_manufacturer_part.html'), name='company-detail-manufacturer-parts'),
|
||||||
url(r'^stock/', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'),
|
url(r'^stock/', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'),
|
||||||
url(r'^purchase-orders/', views.CompanyDetail.as_view(template_name='company/purchase_orders.html'), name='company-detail-purchase-orders'),
|
url(r'^purchase-orders/', views.CompanyDetail.as_view(template_name='company/purchase_orders.html'), name='company-detail-purchase-orders'),
|
||||||
url(r'^assigned-stock/', views.CompanyDetail.as_view(template_name='company/assigned_stock.html'), name='company-detail-assigned-stock'),
|
url(r'^assigned-stock/', views.CompanyDetail.as_view(template_name='company/assigned_stock.html'), name='company-detail-assigned-stock'),
|
||||||
@ -52,9 +53,26 @@ price_break_urls = [
|
|||||||
url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
|
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'^(?P<pk>\d+)/', include(manufacturer_part_detail_urls)),
|
||||||
|
]
|
||||||
|
|
||||||
supplier_part_detail_urls = [
|
supplier_part_detail_urls = [
|
||||||
url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
|
url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
|
||||||
|
|
||||||
|
url(r'^manufacturers/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_manufacturers.html'), name='supplier-part-manufacturers'),
|
||||||
url(r'^pricing/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_pricing.html'), name='supplier-part-pricing'),
|
url(r'^pricing/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_pricing.html'), name='supplier-part-pricing'),
|
||||||
url(r'^orders/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_orders.html'), name='supplier-part-orders'),
|
url(r'^orders/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_orders.html'), name='supplier-part-orders'),
|
||||||
url(r'^stock/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_stock.html'), name='supplier-part-stock'),
|
url(r'^stock/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_stock.html'), name='supplier-part-stock'),
|
||||||
|
@ -24,6 +24,7 @@ from InvenTree.helpers import str2bool
|
|||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
from .models import ManufacturerPart
|
||||||
from .models import SupplierPart
|
from .models import SupplierPart
|
||||||
from .models import SupplierPriceBreak
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ from part.models import Part
|
|||||||
|
|
||||||
from .forms import EditCompanyForm
|
from .forms import EditCompanyForm
|
||||||
from .forms import CompanyImageForm
|
from .forms import CompanyImageForm
|
||||||
|
from .forms import EditManufacturerPartForm
|
||||||
from .forms import EditSupplierPartForm
|
from .forms import EditSupplierPartForm
|
||||||
from .forms import EditPriceBreakForm
|
from .forms import EditPriceBreakForm
|
||||||
from .forms import CompanyImageDownloadForm
|
from .forms import CompanyImageDownloadForm
|
||||||
@ -331,6 +333,177 @@ class CompanyDelete(AjaxDeleteView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartDetail(DetailView):
|
||||||
|
""" Detail view for ManufacturerPart """
|
||||||
|
model = ManufacturerPart
|
||||||
|
template_name = 'company/manufacturer_part_detail.html'
|
||||||
|
context_object_name = 'part'
|
||||||
|
queryset = ManufacturerPart.objects.all()
|
||||||
|
permission_required = 'purchase_order.view'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartEdit(AjaxUpdateView):
|
||||||
|
""" Update view for editing ManufacturerPart """
|
||||||
|
|
||||||
|
model = ManufacturerPart
|
||||||
|
context_object_name = 'part'
|
||||||
|
form_class = EditManufacturerPartForm
|
||||||
|
ajax_template_name = 'modal_form.html'
|
||||||
|
ajax_form_title = _('Edit Manufacturer Part')
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartCreate(AjaxCreateView):
|
||||||
|
""" Create view for making new ManufacturerPart """
|
||||||
|
|
||||||
|
model = ManufacturerPart
|
||||||
|
form_class = EditManufacturerPartForm
|
||||||
|
ajax_template_name = 'company/manufacturer_part_create.html'
|
||||||
|
ajax_form_title = _('Create New Manufacturer Part')
|
||||||
|
context_object_name = 'part'
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
"""
|
||||||
|
Supply context data to the form
|
||||||
|
"""
|
||||||
|
|
||||||
|
ctx = super().get_context_data()
|
||||||
|
|
||||||
|
# Add 'part' object
|
||||||
|
form = self.get_form()
|
||||||
|
|
||||||
|
part = form['part'].value()
|
||||||
|
|
||||||
|
try:
|
||||||
|
part = Part.objects.get(pk=part)
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
part = None
|
||||||
|
|
||||||
|
ctx['part'] = part
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
""" Create Form instance to create a new ManufacturerPart object.
|
||||||
|
Hide some fields if they are not appropriate in context
|
||||||
|
"""
|
||||||
|
form = super(AjaxCreateView, self).get_form()
|
||||||
|
|
||||||
|
if form.initial.get('part', None):
|
||||||
|
# Hide the part field
|
||||||
|
form.fields['part'].widget = HiddenInput()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
""" Provide initial data for new ManufacturerPart:
|
||||||
|
|
||||||
|
- If 'manufacturer_id' provided, pre-fill manufacturer field
|
||||||
|
- If 'part_id' provided, pre-fill part field
|
||||||
|
"""
|
||||||
|
initials = super(ManufacturerPartCreate, self).get_initial().copy()
|
||||||
|
|
||||||
|
manufacturer_id = self.get_param('manufacturer')
|
||||||
|
part_id = self.get_param('part')
|
||||||
|
|
||||||
|
if manufacturer_id:
|
||||||
|
try:
|
||||||
|
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
|
||||||
|
except (ValueError, Company.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if part_id:
|
||||||
|
try:
|
||||||
|
initials['part'] = Part.objects.get(pk=part_id)
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return initials
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartDelete(AjaxDeleteView):
|
||||||
|
""" Delete view for removing a ManufacturerPart.
|
||||||
|
|
||||||
|
ManufacturerParts can be deleted using a variety of 'selectors'.
|
||||||
|
|
||||||
|
- ?part=<pk> -> Delete a single ManufacturerPart object
|
||||||
|
- ?parts=[] -> Delete a list of ManufacturerPart objects
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
success_url = '/manufacturer/'
|
||||||
|
ajax_template_name = 'company/manufacturer_part_delete.html'
|
||||||
|
ajax_form_title = _('Delete Manufacturer Part')
|
||||||
|
|
||||||
|
role_required = 'purchase_order.delete'
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
ctx = {}
|
||||||
|
|
||||||
|
ctx['parts'] = self.parts
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_parts(self):
|
||||||
|
""" Determine which ManufacturerPart object(s) the user wishes to delete.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.parts = []
|
||||||
|
|
||||||
|
# User passes a single ManufacturerPart ID
|
||||||
|
if 'part' in self.request.GET:
|
||||||
|
try:
|
||||||
|
self.parts.append(ManufacturerPart.objects.get(pk=self.request.GET.get('part')))
|
||||||
|
except (ValueError, ManufacturerPart.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif 'parts[]' in self.request.GET:
|
||||||
|
|
||||||
|
part_id_list = self.request.GET.getlist('parts[]')
|
||||||
|
|
||||||
|
self.parts = ManufacturerPart.objects.filter(id__in=part_id_list)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.request = request
|
||||||
|
self.get_parts()
|
||||||
|
|
||||||
|
return self.renderJsonResponse(request, form=self.get_form())
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
""" Handle the POST action for deleting ManufacturerPart object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.request = request
|
||||||
|
self.parts = []
|
||||||
|
|
||||||
|
for item in self.request.POST:
|
||||||
|
if item.startswith('manufacturer-part-'):
|
||||||
|
pk = item.replace('manufacturer-part-', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.parts.append(ManufacturerPart.objects.get(pk=pk))
|
||||||
|
except (ValueError, ManufacturerPart.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
confirm = str2bool(self.request.POST.get('confirm_delete', False))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'form_valid': confirm,
|
||||||
|
}
|
||||||
|
|
||||||
|
if confirm:
|
||||||
|
for part in self.parts:
|
||||||
|
part.delete()
|
||||||
|
|
||||||
|
return self.renderJsonResponse(self.request, data=data, form=self.get_form())
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartDetail(DetailView):
|
class SupplierPartDetail(DetailView):
|
||||||
""" Detail view for SupplierPart """
|
""" Detail view for SupplierPart """
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
@ -354,11 +527,25 @@ class SupplierPartEdit(AjaxUpdateView):
|
|||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = _('Edit Supplier Part')
|
ajax_form_title = _('Edit Supplier Part')
|
||||||
|
|
||||||
|
def save(self, supplier_part, form, **kwargs):
|
||||||
|
""" Process ManufacturerPart data """
|
||||||
|
|
||||||
|
manufacturer = form.cleaned_data.get('manufacturer', None)
|
||||||
|
MPN = form.cleaned_data.get('MPN', None)
|
||||||
|
kwargs = {'manufacturer': manufacturer,
|
||||||
|
'MPN': MPN,
|
||||||
|
}
|
||||||
|
supplier_part.save(**kwargs)
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
form = super().get_form()
|
form = super().get_form()
|
||||||
|
|
||||||
supplier_part = self.get_object()
|
supplier_part = self.get_object()
|
||||||
|
|
||||||
|
# Hide Manufacturer fields
|
||||||
|
form.fields['manufacturer'].widget = HiddenInput()
|
||||||
|
form.fields['MPN'].widget = HiddenInput()
|
||||||
|
|
||||||
# It appears that hiding a MoneyField fails validation
|
# It appears that hiding a MoneyField fails validation
|
||||||
# Therefore the idea to set the value before hiding
|
# Therefore the idea to set the value before hiding
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@ -368,6 +555,19 @@ class SupplierPartEdit(AjaxUpdateView):
|
|||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
""" Fetch data from ManufacturerPart """
|
||||||
|
|
||||||
|
initials = super(SupplierPartEdit, self).get_initial().copy()
|
||||||
|
|
||||||
|
supplier_part = self.get_object()
|
||||||
|
|
||||||
|
if supplier_part.manufacturer_part:
|
||||||
|
initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
|
||||||
|
initials['MPN'] = supplier_part.manufacturer_part.MPN
|
||||||
|
|
||||||
|
return initials
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartCreate(AjaxCreateView):
|
class SupplierPartCreate(AjaxCreateView):
|
||||||
""" Create view for making new SupplierPart """
|
""" Create view for making new SupplierPart """
|
||||||
@ -415,6 +615,14 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
# Save the supplier part object
|
# Save the supplier part object
|
||||||
supplier_part = super().save(form)
|
supplier_part = super().save(form)
|
||||||
|
|
||||||
|
# Process manufacturer data
|
||||||
|
manufacturer = form.cleaned_data.get('manufacturer', None)
|
||||||
|
MPN = form.cleaned_data.get('MPN', None)
|
||||||
|
kwargs = {'manufacturer': manufacturer,
|
||||||
|
'MPN': MPN,
|
||||||
|
}
|
||||||
|
supplier_part.save(**kwargs)
|
||||||
|
|
||||||
single_pricing = form.cleaned_data.get('single_pricing', None)
|
single_pricing = form.cleaned_data.get('single_pricing', None)
|
||||||
|
|
||||||
if single_pricing:
|
if single_pricing:
|
||||||
@ -433,6 +641,12 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
# Hide the part field
|
# Hide the part field
|
||||||
form.fields['part'].widget = HiddenInput()
|
form.fields['part'].widget = HiddenInput()
|
||||||
|
|
||||||
|
if form.initial.get('manufacturer', None):
|
||||||
|
# Hide the manufacturer field
|
||||||
|
form.fields['manufacturer'].widget = HiddenInput()
|
||||||
|
# Hide the MPN field
|
||||||
|
form.fields['MPN'].widget = HiddenInput()
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
@ -446,6 +660,7 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
manufacturer_id = self.get_param('manufacturer')
|
manufacturer_id = self.get_param('manufacturer')
|
||||||
supplier_id = self.get_param('supplier')
|
supplier_id = self.get_param('supplier')
|
||||||
part_id = self.get_param('part')
|
part_id = self.get_param('part')
|
||||||
|
manufacturer_part_id = self.get_param('manufacturer_part')
|
||||||
|
|
||||||
supplier = None
|
supplier = None
|
||||||
|
|
||||||
@ -461,6 +676,16 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
|
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
|
||||||
except (ValueError, Company.DoesNotExist):
|
except (ValueError, Company.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if manufacturer_part_id:
|
||||||
|
try:
|
||||||
|
# Get ManufacturerPart instance information
|
||||||
|
manufacturer_part_obj = ManufacturerPart.objects.get(pk=manufacturer_part_id)
|
||||||
|
initials['part'] = Part.objects.get(pk=manufacturer_part_obj.part.id)
|
||||||
|
initials['manufacturer'] = manufacturer_part_obj.manufacturer.id
|
||||||
|
initials['MPN'] = manufacturer_part_obj.MPN
|
||||||
|
except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
if part_id:
|
if part_id:
|
||||||
try:
|
try:
|
||||||
@ -493,7 +718,7 @@ class SupplierPartDelete(AjaxDeleteView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
success_url = '/supplier/'
|
success_url = '/supplier/'
|
||||||
ajax_template_name = 'company/partdelete.html'
|
ajax_template_name = 'company/supplier_part_delete.html'
|
||||||
ajax_form_title = _('Delete Supplier Part')
|
ajax_form_title = _('Delete Supplier Part')
|
||||||
|
|
||||||
role_required = 'purchase_order.delete'
|
role_required = 'purchase_order.delete'
|
||||||
|
@ -253,10 +253,12 @@ class StockItemLabel(LabelTemplate):
|
|||||||
'part': stock_item.part,
|
'part': stock_item.part,
|
||||||
'name': stock_item.part.full_name,
|
'name': stock_item.part.full_name,
|
||||||
'ipn': stock_item.part.IPN,
|
'ipn': stock_item.part.IPN,
|
||||||
|
'revision': stock_item.part.revision,
|
||||||
'quantity': normalize(stock_item.quantity),
|
'quantity': normalize(stock_item.quantity),
|
||||||
'serial': stock_item.serial,
|
'serial': stock_item.serial,
|
||||||
'uid': stock_item.uid,
|
'uid': stock_item.uid,
|
||||||
'qr_data': stock_item.format_barcode(brief=True),
|
'qr_data': stock_item.format_barcode(brief=True),
|
||||||
|
'qr_url': stock_item.format_barcode(url=True, request=request),
|
||||||
'tests': stock_item.testResultMap()
|
'tests': stock_item.testResultMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: inventree1\n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-21 10:02\n"
|
"PO-Revision-Date: 2021-04-21 10:02\n"
|
||||||
@ -11,11 +11,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Crowdin-Project: inventree1\n"
|
|
||||||
"X-Crowdin-Project-ID: 450990\n"
|
|
||||||
"X-Crowdin-Language: de\n"
|
|
||||||
"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
|
|
||||||
"X-Crowdin-File-ID: 98\n"
|
|
||||||
|
|
||||||
#: InvenTree/api.py:64
|
#: InvenTree/api.py:64
|
||||||
msgid "API endpoint not found"
|
msgid "API endpoint not found"
|
||||||
@ -7230,4 +7225,3 @@ msgstr "Berechtigungen Einträge zu ändern"
|
|||||||
#: users/models.py:184
|
#: users/models.py:184
|
||||||
msgid "Permission to delete items"
|
msgid "Permission to delete items"
|
||||||
msgstr "Berechtigung Einträge zu löschen"
|
msgstr "Berechtigung Einträge zu löschen"
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: inventree1\n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-21 09:33\n"
|
"PO-Revision-Date: 2021-04-21 09:33\n"
|
||||||
@ -11,11 +11,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Crowdin-Project: inventree1\n"
|
|
||||||
"X-Crowdin-Project-ID: 450990\n"
|
|
||||||
"X-Crowdin-Language: es-ES\n"
|
|
||||||
"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
|
|
||||||
"X-Crowdin-File-ID: 98\n"
|
|
||||||
|
|
||||||
#: InvenTree/api.py:64
|
#: InvenTree/api.py:64
|
||||||
msgid "API endpoint not found"
|
msgid "API endpoint not found"
|
||||||
@ -7226,4 +7221,3 @@ msgstr ""
|
|||||||
#: users/models.py:184
|
#: users/models.py:184
|
||||||
msgid "Permission to delete items"
|
msgid "Permission to delete items"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
BIN
InvenTree/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: inventree1\n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-21 09:33\n"
|
"PO-Revision-Date: 2021-04-21 09:33\n"
|
||||||
@ -11,11 +11,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
"X-Crowdin-Project: inventree1\n"
|
|
||||||
"X-Crowdin-Project-ID: 450990\n"
|
|
||||||
"X-Crowdin-Language: fr\n"
|
|
||||||
"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
|
|
||||||
"X-Crowdin-File-ID: 98\n"
|
|
||||||
|
|
||||||
#: InvenTree/api.py:64
|
#: InvenTree/api.py:64
|
||||||
msgid "API endpoint not found"
|
msgid "API endpoint not found"
|
||||||
|
BIN
InvenTree/locale/it/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/it/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: inventree1\n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-21 09:33\n"
|
"PO-Revision-Date: 2021-04-21 09:33\n"
|
||||||
@ -11,11 +11,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Crowdin-Project: inventree1\n"
|
|
||||||
"X-Crowdin-Project-ID: 450990\n"
|
|
||||||
"X-Crowdin-Language: it\n"
|
|
||||||
"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
|
|
||||||
"X-Crowdin-File-ID: 98\n"
|
|
||||||
|
|
||||||
#: InvenTree/api.py:64
|
#: InvenTree/api.py:64
|
||||||
msgid "API endpoint not found"
|
msgid "API endpoint not found"
|
||||||
|
BIN
InvenTree/locale/ja/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/ja/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: inventree1\n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-21 09:33\n"
|
"PO-Revision-Date: 2021-04-21 09:33\n"
|
||||||
@ -11,11 +11,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
"X-Crowdin-Project: inventree1\n"
|
|
||||||
"X-Crowdin-Project-ID: 450990\n"
|
|
||||||
"X-Crowdin-Language: ja\n"
|
|
||||||
"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
|
|
||||||
"X-Crowdin-File-ID: 98\n"
|
|
||||||
|
|
||||||
#: InvenTree/api.py:64
|
#: InvenTree/api.py:64
|
||||||
msgid "API endpoint not found"
|
msgid "API endpoint not found"
|
||||||
@ -7226,4 +7221,3 @@ msgstr ""
|
|||||||
#: users/models.py:184
|
#: users/models.py:184
|
||||||
msgid "Permission to delete items"
|
msgid "Permission to delete items"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
BIN
InvenTree/locale/pl/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/pl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: inventree1\n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-21 09:33\n"
|
"PO-Revision-Date: 2021-04-21 09:33\n"
|
||||||
@ -11,11 +11,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||||
"X-Crowdin-Project: inventree1\n"
|
|
||||||
"X-Crowdin-Project-ID: 450990\n"
|
|
||||||
"X-Crowdin-Language: pl\n"
|
|
||||||
"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
|
|
||||||
"X-Crowdin-File-ID: 98\n"
|
|
||||||
|
|
||||||
#: InvenTree/api.py:64
|
#: InvenTree/api.py:64
|
||||||
msgid "API endpoint not found"
|
msgid "API endpoint not found"
|
||||||
@ -7226,4 +7221,3 @@ msgstr ""
|
|||||||
#: users/models.py:184
|
#: users/models.py:184
|
||||||
msgid "Permission to delete items"
|
msgid "Permission to delete items"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
BIN
InvenTree/locale/ru/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/ru/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: inventree1\n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-21 09:33\n"
|
"PO-Revision-Date: 2021-04-21 09:33\n"
|
||||||
@ -11,11 +11,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
||||||
"X-Crowdin-Project: inventree1\n"
|
|
||||||
"X-Crowdin-Project-ID: 450990\n"
|
|
||||||
"X-Crowdin-Language: ru\n"
|
|
||||||
"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
|
|
||||||
"X-Crowdin-File-ID: 98\n"
|
|
||||||
|
|
||||||
#: InvenTree/api.py:64
|
#: InvenTree/api.py:64
|
||||||
msgid "API endpoint not found"
|
msgid "API endpoint not found"
|
||||||
@ -7226,4 +7221,3 @@ msgstr ""
|
|||||||
#: users/models.py:184
|
#: users/models.py:184
|
||||||
msgid "Permission to delete items"
|
msgid "Permission to delete items"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
BIN
InvenTree/locale/zh/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/zh/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: inventree1\n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-21 09:33\n"
|
"PO-Revision-Date: 2021-04-21 09:33\n"
|
||||||
@ -11,11 +11,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
"X-Crowdin-Project: inventree1\n"
|
|
||||||
"X-Crowdin-Project-ID: 450990\n"
|
|
||||||
"X-Crowdin-Language: zh-CN\n"
|
|
||||||
"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
|
|
||||||
"X-Crowdin-File-ID: 98\n"
|
|
||||||
|
|
||||||
#: InvenTree/api.py:64
|
#: InvenTree/api.py:64
|
||||||
msgid "API endpoint not found"
|
msgid "API endpoint not found"
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{% load status_codes %}
|
{% load status_codes %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Purchase Order" %}
|
{% inventree_title %} | {% trans "Purchase Order" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
{% trans 'Mark this order as complete?' %}
|
{% trans 'Mark this order as complete?' %}
|
||||||
{% if not order.is_complete %}
|
{% if not order.is_complete %}
|
||||||
<div class='alert alert-warning alert-block'>
|
<div class='alert alert-warning alert-block'>
|
||||||
{%trans 'This order has line items which have not been marked as received.
|
{% trans 'This order has line items which have not been marked as received.' %}
|
||||||
Marking this order as complete will remove these line items.' %}
|
{% trans 'Marking this order as complete will remove these line items.' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{% if not part.order_supplier %}
|
{% if not part.order_supplier %}
|
||||||
<span class='help-inline'>{% trans "Select a supplier for" %} <i>{{ part.name }}</i></span>
|
<span class='help-inline'>{% blocktrans with name=part.name %}Select a supplier for <i>{{name}}</i>{% endblocktrans %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
<button
|
<button
|
||||||
class='btn btn-default btn-create'
|
class='btn btn-default btn-create'
|
||||||
id='new_po_{{ supplier.id }}'
|
id='new_po_{{ supplier.id }}'
|
||||||
title='{% trans "Create new purchase order for {{ supplier.name }}" %}'
|
title='{% blocktrans with name=supplier.name %}Create new purchase order for {{name}}{% endblocktrans %}'
|
||||||
type='button'
|
type='button'
|
||||||
supplierid='{{ supplier.id }}'
|
supplierid='{{ supplier.id }}'
|
||||||
onclick='newPurchaseOrderFromOrderWizard()'>
|
onclick='newPurchaseOrderFromOrderWizard()'>
|
||||||
@ -65,7 +65,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{% if not supplier.selected_purchase_order %}
|
{% if not supplier.selected_purchase_order %}
|
||||||
<span class='help-inline'>{% trans "Select a purchase order for" %} {{ supplier.name }}</span>
|
<span class='help-inline'>{% blocktrans with name=supplier.name %}Select a purchase order for {{name}}{% endblocktrans %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -181,6 +181,13 @@ $("#po-table").inventreeTable({
|
|||||||
sortName: 'part__MPN',
|
sortName: 'part__MPN',
|
||||||
field: 'supplier_part_detail.MPN',
|
field: 'supplier_part_detail.MPN',
|
||||||
title: '{% trans "MPN" %}',
|
title: '{% trans "MPN" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
if (row.supplier_part_detail.manufacturer_part) {
|
||||||
|
return renderLink(value, `/manufacturer-part/${row.supplier_part_detail.manufacturer_part.pk}/`);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Purchase Orders" %}
|
{% inventree_title %} | {% trans "Purchase Orders" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
|
|
||||||
{% trans "Receive outstanding parts for" %} <b>{{ order }}</b> - <i>{{ order.description }}</i>
|
{% blocktrans with desc=order.description %}Receive outstanding parts for <b>{{order}}</b> - <i>{{desc}}</i>{% endblocktrans %}
|
||||||
|
|
||||||
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{% load status_codes %}
|
{% load status_codes %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Sales Order" %}
|
{% inventree_title %} | {% trans "Sales Order" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pre_content %}
|
{% block pre_content %}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Sales Orders" %}
|
{% inventree_title %} | {% trans "Sales Orders" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -8,6 +8,7 @@ from __future__ import unicode_literals
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db.models import Q, F, Count, Prefetch, Sum
|
from django.db.models import Q, F, Count, Prefetch, Sum
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -36,7 +37,7 @@ from InvenTree.status_codes import BuildStatus
|
|||||||
|
|
||||||
class PartCategoryTree(TreeSerializer):
|
class PartCategoryTree(TreeSerializer):
|
||||||
|
|
||||||
title = "Parts"
|
title = _("Parts")
|
||||||
model = PartCategory
|
model = PartCategory
|
||||||
|
|
||||||
queryset = PartCategory.objects.all()
|
queryset = PartCategory.objects.all()
|
||||||
@ -59,28 +60,43 @@ class CategoryList(generics.ListCreateAPIView):
|
|||||||
queryset = PartCategory.objects.all()
|
queryset = PartCategory.objects.all()
|
||||||
serializer_class = part_serializers.CategorySerializer
|
serializer_class = part_serializers.CategorySerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def filter_queryset(self, queryset):
|
||||||
"""
|
"""
|
||||||
Custom filtering:
|
Custom filtering:
|
||||||
- Allow filtering by "null" parent to retrieve top-level part categories
|
- Allow filtering by "null" parent to retrieve top-level part categories
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cat_id = self.request.query_params.get('parent', None)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
queryset = super().get_queryset()
|
params = self.request.query_params
|
||||||
|
|
||||||
if cat_id is not None:
|
cat_id = params.get('parent', None)
|
||||||
|
|
||||||
|
cascade = str2bool(params.get('cascade', False))
|
||||||
|
|
||||||
|
# Do not filter by category
|
||||||
|
if cat_id is None:
|
||||||
|
pass
|
||||||
|
# Look for top-level categories
|
||||||
|
elif isNull(cat_id):
|
||||||
|
|
||||||
# Look for top-level categories
|
if not cascade:
|
||||||
if isNull(cat_id):
|
|
||||||
queryset = queryset.filter(parent=None)
|
queryset = queryset.filter(parent=None)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
cat_id = int(cat_id)
|
category = PartCategory.objects.get(pk=cat_id)
|
||||||
queryset = queryset.filter(parent=cat_id)
|
|
||||||
except ValueError:
|
if cascade:
|
||||||
pass
|
parents = category.get_descendants(include_self=True)
|
||||||
|
parent_ids = [p.id for p in parents]
|
||||||
|
|
||||||
|
queryset = queryset.filter(parent__in=parent_ids)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(parent=category)
|
||||||
|
|
||||||
|
except (ValueError, PartCategory.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from InvenTree.helpers import DownloadFile, GetExportFormats
|
|||||||
|
|
||||||
from .admin import BomItemResource
|
from .admin import BomItemResource
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from company.models import SupplierPart
|
from company.models import ManufacturerPart, SupplierPart
|
||||||
|
|
||||||
|
|
||||||
def IsValidBOMFormat(fmt):
|
def IsValidBOMFormat(fmt):
|
||||||
@ -49,7 +49,7 @@ def MakeBomTemplate(fmt):
|
|||||||
return DownloadFile(data, filename)
|
return DownloadFile(data, filename)
|
||||||
|
|
||||||
|
|
||||||
def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False):
|
def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False, manufacturer_data=False):
|
||||||
""" Export a BOM (Bill of Materials) for a given part.
|
""" Export a BOM (Bill of Materials) for a given part.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -160,7 +160,123 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
# Add stock columns to dataset
|
# Add stock columns to dataset
|
||||||
add_columns_to_dataset(stock_cols, len(bom_items))
|
add_columns_to_dataset(stock_cols, len(bom_items))
|
||||||
|
|
||||||
if supplier_data:
|
if manufacturer_data and supplier_data:
|
||||||
|
"""
|
||||||
|
If requested, add extra columns for each SupplierPart and ManufacturerPart associated with each line item
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Expand dataset with manufacturer parts
|
||||||
|
manufacturer_headers = [
|
||||||
|
_('Manufacturer'),
|
||||||
|
_('MPN'),
|
||||||
|
]
|
||||||
|
|
||||||
|
supplier_headers = [
|
||||||
|
_('Supplier'),
|
||||||
|
_('SKU'),
|
||||||
|
]
|
||||||
|
|
||||||
|
manufacturer_cols = {}
|
||||||
|
|
||||||
|
for b_idx, bom_item in enumerate(bom_items):
|
||||||
|
# Get part instance
|
||||||
|
b_part = bom_item.sub_part
|
||||||
|
|
||||||
|
# Filter manufacturer parts
|
||||||
|
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk)
|
||||||
|
manufacturer_parts = manufacturer_parts.prefetch_related('supplier_parts')
|
||||||
|
|
||||||
|
# Process manufacturer part
|
||||||
|
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts):
|
||||||
|
|
||||||
|
if manufacturer_part:
|
||||||
|
manufacturer_name = manufacturer_part.manufacturer.name
|
||||||
|
else:
|
||||||
|
manufacturer_name = ''
|
||||||
|
|
||||||
|
manufacturer_mpn = manufacturer_part.MPN
|
||||||
|
|
||||||
|
# Generate column names for this manufacturer
|
||||||
|
k_man = manufacturer_headers[0] + "_" + str(manufacturer_idx)
|
||||||
|
k_mpn = manufacturer_headers[1] + "_" + str(manufacturer_idx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
manufacturer_cols[k_man].update({b_idx: manufacturer_name})
|
||||||
|
manufacturer_cols[k_mpn].update({b_idx: manufacturer_mpn})
|
||||||
|
except KeyError:
|
||||||
|
manufacturer_cols[k_man] = {b_idx: manufacturer_name}
|
||||||
|
manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn}
|
||||||
|
|
||||||
|
# Process supplier parts
|
||||||
|
for supplier_idx, supplier_part in enumerate(manufacturer_part.supplier_parts.all()):
|
||||||
|
|
||||||
|
if supplier_part.supplier:
|
||||||
|
supplier_name = supplier_part.supplier.name
|
||||||
|
else:
|
||||||
|
supplier_name = ''
|
||||||
|
|
||||||
|
supplier_sku = supplier_part.SKU
|
||||||
|
|
||||||
|
# Generate column names for this supplier
|
||||||
|
k_sup = str(supplier_headers[0]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)
|
||||||
|
k_sku = str(supplier_headers[1]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
manufacturer_cols[k_sup].update({b_idx: supplier_name})
|
||||||
|
manufacturer_cols[k_sku].update({b_idx: supplier_sku})
|
||||||
|
except KeyError:
|
||||||
|
manufacturer_cols[k_sup] = {b_idx: supplier_name}
|
||||||
|
manufacturer_cols[k_sku] = {b_idx: supplier_sku}
|
||||||
|
|
||||||
|
# Add manufacturer columns to dataset
|
||||||
|
add_columns_to_dataset(manufacturer_cols, len(bom_items))
|
||||||
|
|
||||||
|
elif manufacturer_data:
|
||||||
|
"""
|
||||||
|
If requested, add extra columns for each ManufacturerPart associated with each line item
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Expand dataset with manufacturer parts
|
||||||
|
manufacturer_headers = [
|
||||||
|
_('Manufacturer'),
|
||||||
|
_('MPN'),
|
||||||
|
]
|
||||||
|
|
||||||
|
manufacturer_cols = {}
|
||||||
|
|
||||||
|
for b_idx, bom_item in enumerate(bom_items):
|
||||||
|
# Get part instance
|
||||||
|
b_part = bom_item.sub_part
|
||||||
|
|
||||||
|
# Filter supplier parts
|
||||||
|
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk)
|
||||||
|
|
||||||
|
for idx, manufacturer_part in enumerate(manufacturer_parts):
|
||||||
|
|
||||||
|
if manufacturer_part:
|
||||||
|
manufacturer_name = manufacturer_part.manufacturer.name
|
||||||
|
else:
|
||||||
|
manufacturer_name = ''
|
||||||
|
|
||||||
|
manufacturer_mpn = manufacturer_part.MPN
|
||||||
|
|
||||||
|
# Add manufacturer data to the manufacturer columns
|
||||||
|
|
||||||
|
# Generate column names for this manufacturer
|
||||||
|
k_man = manufacturer_headers[0] + "_" + str(idx)
|
||||||
|
k_mpn = manufacturer_headers[1] + "_" + str(idx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
manufacturer_cols[k_man].update({b_idx: manufacturer_name})
|
||||||
|
manufacturer_cols[k_mpn].update({b_idx: manufacturer_mpn})
|
||||||
|
except KeyError:
|
||||||
|
manufacturer_cols[k_man] = {b_idx: manufacturer_name}
|
||||||
|
manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn}
|
||||||
|
|
||||||
|
# Add manufacturer columns to dataset
|
||||||
|
add_columns_to_dataset(manufacturer_cols, len(bom_items))
|
||||||
|
|
||||||
|
elif supplier_data:
|
||||||
"""
|
"""
|
||||||
If requested, add extra columns for each SupplierPart associated with each line item
|
If requested, add extra columns for each SupplierPart associated with each line item
|
||||||
"""
|
"""
|
||||||
@ -169,8 +285,6 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
manufacturer_headers = [
|
manufacturer_headers = [
|
||||||
_('Supplier'),
|
_('Supplier'),
|
||||||
_('SKU'),
|
_('SKU'),
|
||||||
_('Manufacturer'),
|
|
||||||
_('MPN'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
manufacturer_cols = {}
|
manufacturer_cols = {}
|
||||||
@ -191,31 +305,18 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
supplier_sku = supplier_part.SKU
|
supplier_sku = supplier_part.SKU
|
||||||
|
|
||||||
if supplier_part.manufacturer:
|
|
||||||
manufacturer_name = supplier_part.manufacturer.name
|
|
||||||
else:
|
|
||||||
manufacturer_name = ''
|
|
||||||
|
|
||||||
manufacturer_mpn = supplier_part.MPN
|
|
||||||
|
|
||||||
# Add manufacturer data to the manufacturer columns
|
# Add manufacturer data to the manufacturer columns
|
||||||
|
|
||||||
# Generate column names for this supplier
|
# Generate column names for this supplier
|
||||||
k_sup = manufacturer_headers[0] + "_" + str(idx)
|
k_sup = manufacturer_headers[0] + "_" + str(idx)
|
||||||
k_sku = manufacturer_headers[1] + "_" + str(idx)
|
k_sku = manufacturer_headers[1] + "_" + str(idx)
|
||||||
k_man = manufacturer_headers[2] + "_" + str(idx)
|
|
||||||
k_mpn = manufacturer_headers[3] + "_" + str(idx)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manufacturer_cols[k_sup].update({b_idx: supplier_name})
|
manufacturer_cols[k_sup].update({b_idx: supplier_name})
|
||||||
manufacturer_cols[k_sku].update({b_idx: supplier_sku})
|
manufacturer_cols[k_sku].update({b_idx: supplier_sku})
|
||||||
manufacturer_cols[k_man].update({b_idx: manufacturer_name})
|
|
||||||
manufacturer_cols[k_mpn].update({b_idx: manufacturer_mpn})
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
manufacturer_cols[k_sup] = {b_idx: supplier_name}
|
manufacturer_cols[k_sup] = {b_idx: supplier_name}
|
||||||
manufacturer_cols[k_sku] = {b_idx: supplier_sku}
|
manufacturer_cols[k_sku] = {b_idx: supplier_sku}
|
||||||
manufacturer_cols[k_man] = {b_idx: manufacturer_name}
|
|
||||||
manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn}
|
|
||||||
|
|
||||||
# Add manufacturer columns to dataset
|
# Add manufacturer columns to dataset
|
||||||
add_columns_to_dataset(manufacturer_cols, len(bom_items))
|
add_columns_to_dataset(manufacturer_cols, len(bom_items))
|
||||||
|
@ -95,9 +95,11 @@ class BomExportForm(forms.Form):
|
|||||||
parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM"))
|
parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM"))
|
||||||
|
|
||||||
stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM"))
|
stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM"))
|
||||||
|
|
||||||
|
manufacturer_data = forms.BooleanField(label=_("Include Manufacturer Data"), required=False, initial=True, help_text=_("Include part manufacturer data in exported BOM"))
|
||||||
|
|
||||||
supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM"))
|
supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM"))
|
||||||
|
|
||||||
def get_choices(self):
|
def get_choices(self):
|
||||||
""" BOM export format choices """
|
""" BOM export format choices """
|
||||||
|
|
||||||
|
@ -1163,7 +1163,16 @@ class Part(MPTTModel):
|
|||||||
Return the total amount of this part allocated to build orders
|
Return the total amount of this part allocated to build orders
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query = self.build_order_allocations().aggregate(total=Coalesce(Sum('quantity'), 0))
|
query = self.build_order_allocations().aggregate(
|
||||||
|
total=Coalesce(
|
||||||
|
Sum(
|
||||||
|
'quantity',
|
||||||
|
output_field=models.DecimalField()
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return query['total']
|
return query['total']
|
||||||
|
|
||||||
@ -1179,7 +1188,16 @@ class Part(MPTTModel):
|
|||||||
Return the tutal quantity of this part allocated to sales orders
|
Return the tutal quantity of this part allocated to sales orders
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query = self.sales_order_allocations().aggregate(total=Coalesce(Sum('quantity'), 0))
|
query = self.sales_order_allocations().aggregate(
|
||||||
|
total=Coalesce(
|
||||||
|
Sum(
|
||||||
|
'quantity',
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return query['total']
|
return query['total']
|
||||||
|
|
||||||
@ -1189,10 +1207,12 @@ class Part(MPTTModel):
|
|||||||
against both build orders and sales orders.
|
against both build orders and sales orders.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return sum([
|
return sum(
|
||||||
self.build_order_allocation_count(),
|
[
|
||||||
self.sales_order_allocation_count(),
|
self.build_order_allocation_count(),
|
||||||
])
|
self.sales_order_allocation_count(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def stock_entries(self, include_variants=True, in_stock=None):
|
def stock_entries(self, include_variants=True, in_stock=None):
|
||||||
""" Return all stock entries for this Part.
|
""" Return all stock entries for this Part.
|
||||||
|
@ -4,6 +4,7 @@ JSON serializers for Part app
|
|||||||
import imghdr
|
import imghdr
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
||||||
@ -208,7 +209,8 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
in_stock=Coalesce(
|
in_stock=Coalesce(
|
||||||
SubquerySum('stock_items__quantity', filter=StockItem.IN_STOCK_FILTER),
|
SubquerySum('stock_items__quantity', filter=StockItem.IN_STOCK_FILTER),
|
||||||
Decimal(0)
|
Decimal(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -227,6 +229,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
building=Coalesce(
|
building=Coalesce(
|
||||||
SubquerySum('builds__quantity', filter=build_filter),
|
SubquerySum('builds__quantity', filter=build_filter),
|
||||||
Decimal(0),
|
Decimal(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -240,9 +243,11 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
ordering=Coalesce(
|
ordering=Coalesce(
|
||||||
SubquerySum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter),
|
SubquerySum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter),
|
||||||
Decimal(0),
|
Decimal(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
) - Coalesce(
|
) - Coalesce(
|
||||||
SubquerySum('supplier_parts__purchase_order_line_items__received', filter=order_filter),
|
SubquerySum('supplier_parts__purchase_order_line_items__received', filter=order_filter),
|
||||||
Decimal(0),
|
Decimal(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -251,6 +256,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
suppliers=Coalesce(
|
suppliers=Coalesce(
|
||||||
SubqueryCount('supplier_parts'),
|
SubqueryCount('supplier_parts'),
|
||||||
Decimal(0),
|
Decimal(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include 'part/category_navbar.html' with tab='parts' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class='panel panel-default panel-inventree'>
|
<div class='panel panel-default panel-inventree'>
|
||||||
@ -100,14 +104,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if category and category.children.all|length > 0 %}
|
|
||||||
{% include "part/subcategories.html" with children=category.children.all collapse_id="categories" %}
|
|
||||||
{% elif children|length > 0 %}
|
|
||||||
{% include "part/subcategories.html" with children=children collapse_id="categories" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% block category_content %}
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
<div id='button-toolbar'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>
|
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>
|
||||||
@ -150,6 +150,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block category_tables %}
|
{% block category_tables %}
|
||||||
{% endblock category_tables %}
|
{% endblock category_tables %}
|
||||||
@ -162,24 +164,10 @@
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
{% if category %}
|
|
||||||
enableNavbar({
|
enableNavbar({
|
||||||
label: 'category',
|
label: 'category',
|
||||||
toggleId: '#category-menu-toggle',
|
toggleId: '#category-menu-toggle',
|
||||||
});
|
});
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
if (inventreeLoadInt("show-part-cats") == 1) {
|
|
||||||
$("#collapse-item-categories").collapse('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#collapse-item-categories").on('shown.bs.collapse', function() {
|
|
||||||
inventreeSave('show-part-cats', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#collapse-item-categories").on('hidden.bs.collapse', function() {
|
|
||||||
inventreeDel('show-part-cats');
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#cat-create").click(function() {
|
$("#cat-create").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% trans 'Are you sure you want to delete category' %} <strong>{{ category.name }}</strong>?
|
{% trans 'Are you sure you want to delete category' %} <strong>{{ category.name }}</strong>?
|
||||||
|
|
||||||
{% if category.children.all|length > 0 %}
|
{% if category.children.all|length > 0 %}
|
||||||
<p>{% trans 'This category contains' %} {{ category.children.all|length }} {% trans 'child categories' %}.<br>
|
<p>{% blocktrans with count=category.children.all|length%}This category contains {{count}} child categories{% endblocktrans %}.<br>
|
||||||
{% trans 'If this category is deleted, these child categories will be moved to the' %}
|
{% trans 'If this category is deleted, these child categories will be moved to the' %}
|
||||||
{% if category.parent %}
|
{% if category.parent %}
|
||||||
<strong>{{ category.parent.name }}</strong> {% trans 'category' %}.
|
<strong>{{ category.parent.name }}</strong> {% trans 'category' %}.
|
||||||
@ -22,9 +22,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if category.parts.all|length > 0 %}
|
{% if category.parts.all|length > 0 %}
|
||||||
<p>{% trans 'This category contains' %} {{ category.parts.all|length }} {% trans 'parts' %}.<br>
|
<p>{% blocktrans with count=category.parts.all|length %}This category contains {{count}} parts{% endblocktrans %}.<br>
|
||||||
{% if category.parent %}
|
{% if category.parent %}
|
||||||
{% trans 'If this category is deleted, these parts will be moved to the parent category' %} {{ category.parent.pathstring }}
|
{% blocktrans with path=category.parent.pathstring %}If this category is deleted, these parts will be moved to the parent category {{path}}{% endblocktrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans 'If this category is deleted, these parts will be moved to the top-level category Teile' %}
|
{% trans 'If this category is deleted, these parts will be moved to the top-level category Teile' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -8,17 +8,34 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item {% if tab == "subcategories" %}active{% endif %}' title='{% trans "Subcategories" %}'>
|
||||||
|
{% if category %}
|
||||||
|
<a href='{% url "category-subcategory" category.id %}'>
|
||||||
|
{% else %}
|
||||||
|
<a href='{% url "category-index-subcategory" %}'>
|
||||||
|
{% endif %}
|
||||||
|
<span class='fas fa-sitemap'></span>
|
||||||
|
{% trans "Subcategories" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Parts" %}'>
|
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Parts" %}'>
|
||||||
|
{% if category %}
|
||||||
<a href='{% url "category-detail" category.id %}'>
|
<a href='{% url "category-detail" category.id %}'>
|
||||||
|
{% else %}
|
||||||
|
<a href='{% url "part-index" %}'>
|
||||||
|
{% endif %}
|
||||||
<span class='fas fa-shapes'></span>
|
<span class='fas fa-shapes'></span>
|
||||||
{% trans "Parts" %}
|
{% trans "Parts" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% if category %}
|
||||||
<li class='list-group-item {% if tab == "parameters" %}active{% endif %}' title='{% trans "Parameters" %}'>
|
<li class='list-group-item {% if tab == "parameters" %}active{% endif %}' title='{% trans "Parameters" %}'>
|
||||||
<a href='{% url "category-parametric" category.id %}'>
|
<a href='{% url "category-parametric" category.id %}'>
|
||||||
<span class='fas fa-tasks'></span>
|
<span class='fas fa-tasks'></span>
|
||||||
{% trans "Parameters" %}
|
{% trans "Parameters" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class='alert alert-info alert-block'>
|
<div class='alert alert-info alert-block'>
|
||||||
<strong>{% trans 'Duplicate Part' %}</strong><br>
|
<strong>{% trans 'Duplicate Part' %}</strong><br>
|
||||||
{% trans 'Make a copy of part' %} '{{ part.full_name }}'.
|
{% blocktrans with full_name=part.full_name %}Make a copy of part '{{full_name}}'.{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if matches %}
|
{% if matches %}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user