mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into price-history
This commit is contained in:
commit
75381cbb7b
2
.github/workflows/coverage.yaml
vendored
2
.github/workflows/coverage.yaml
vendored
@ -16,6 +16,8 @@ jobs:
|
|||||||
INVENTREE_DB_NAME: './test_db.sqlite'
|
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||||
INVENTREE_DEBUG: info
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
18
.github/workflows/docker_build.yaml
vendored
Normal file
18
.github/workflows/docker_build.yaml
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Test that the docker file builds correctly
|
||||||
|
|
||||||
|
name: Docker
|
||||||
|
|
||||||
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build Server Image
|
||||||
|
run: cd docker/inventree && docker build . --tag inventree:$(date +%s)
|
||||||
|
- name: Build nginx Image
|
||||||
|
run: cd docker/nginx && docker build . --tag nxinx:$(date +%s)
|
||||||
|
|
38
.github/workflows/docker_publish.yaml
vendored
Normal file
38
.github/workflows/docker_publish.yaml
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Publish docker images to dockerhub
|
||||||
|
|
||||||
|
name: Docker Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
server_image:
|
||||||
|
name: Push InvenTree web server 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/inventree
|
||||||
|
tag_with_ref: true
|
||||||
|
dockerfile: docker/inventree/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
|
46
.github/workflows/mariadb.yaml
vendored
46
.github/workflows/mariadb.yaml
vendored
@ -1,46 +0,0 @@
|
|||||||
name: MariaDB
|
|
||||||
|
|
||||||
on: ["push", "pull_request"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
# Database backend configuration
|
|
||||||
INVENTREE_DB_ENGINE: django.db.backends.mysql
|
|
||||||
INVENTREE_DB_NAME: inventree
|
|
||||||
INVENTREE_DB_USER: root
|
|
||||||
INVENTREE_DB_PASSWORD: password
|
|
||||||
INVENTREE_DB_HOST: '127.0.0.1'
|
|
||||||
INVENTREE_DB_PORT: 3306
|
|
||||||
INVENTREE_DEBUG: info
|
|
||||||
|
|
||||||
services:
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:latest
|
|
||||||
env:
|
|
||||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
|
||||||
MYSQL_DATABASE: inventree
|
|
||||||
MYSQL_USER: inventree
|
|
||||||
MYSQL_PASSWORD: password
|
|
||||||
MYSQL_ROOT_PASSWORD: password
|
|
||||||
ports:
|
|
||||||
- 3306:3306
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.7
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get install mysql-server libmysqlclient-dev
|
|
||||||
pip3 install invoke
|
|
||||||
pip3 install mysqlclient
|
|
||||||
invoke install
|
|
||||||
- name: Run Tests
|
|
||||||
run: invoke test
|
|
2
.github/workflows/mysql.yaml
vendored
2
.github/workflows/mysql.yaml
vendored
@ -18,6 +18,8 @@ jobs:
|
|||||||
INVENTREE_DB_HOST: '127.0.0.1'
|
INVENTREE_DB_HOST: '127.0.0.1'
|
||||||
INVENTREE_DB_PORT: 3306
|
INVENTREE_DB_PORT: 3306
|
||||||
INVENTREE_DEBUG: info
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
|
2
.github/workflows/postgresql.yaml
vendored
2
.github/workflows/postgresql.yaml
vendored
@ -18,6 +18,8 @@ jobs:
|
|||||||
INVENTREE_DB_HOST: '127.0.0.1'
|
INVENTREE_DB_HOST: '127.0.0.1'
|
||||||
INVENTREE_DB_PORT: 5432
|
INVENTREE_DB_PORT: 5432
|
||||||
INVENTREE_DEBUG: info
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
@ -19,11 +19,12 @@ from rest_framework.views import APIView
|
|||||||
|
|
||||||
from .views import AjaxView
|
from .views import AjaxView
|
||||||
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
||||||
|
from .status import is_worker_running
|
||||||
|
|
||||||
from plugins import plugins as inventree_plugins
|
from plugins import plugins as inventree_plugins
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
logger.info("Loading action plugins...")
|
logger.info("Loading action plugins...")
|
||||||
@ -44,6 +45,7 @@ class InfoView(AjaxView):
|
|||||||
'version': inventreeVersion(),
|
'version': inventreeVersion(),
|
||||||
'instance': inventreeInstanceName(),
|
'instance': inventreeInstanceName(),
|
||||||
'apiVersion': inventreeApiVersion(),
|
'apiVersion': inventreeApiVersion(),
|
||||||
|
'worker_running': is_worker_running(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
44
InvenTree/InvenTree/apps.py
Normal file
44
InvenTree/InvenTree/apps.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
|
|
||||||
|
import InvenTree.tasks
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeConfig(AppConfig):
|
||||||
|
name = 'InvenTree'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
|
||||||
|
self.start_background_tasks()
|
||||||
|
|
||||||
|
def start_background_tasks(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django_q.models import Schedule
|
||||||
|
except (AppRegistryNotReady):
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Starting background tasks...")
|
||||||
|
|
||||||
|
InvenTree.tasks.schedule_task(
|
||||||
|
'InvenTree.tasks.delete_successful_tasks',
|
||||||
|
schedule_type=Schedule.DAILY,
|
||||||
|
)
|
||||||
|
|
||||||
|
InvenTree.tasks.schedule_task(
|
||||||
|
'InvenTree.tasks.check_for_updates',
|
||||||
|
schedule_type=Schedule.DAILY
|
||||||
|
)
|
||||||
|
|
||||||
|
InvenTree.tasks.schedule_task(
|
||||||
|
'InvenTree.tasks.heartbeat',
|
||||||
|
schedule_type=Schedule.MINUTES,
|
||||||
|
minutes=15
|
||||||
|
)
|
@ -30,10 +30,23 @@ def health_status(request):
|
|||||||
|
|
||||||
request._inventree_health_status = True
|
request._inventree_health_status = True
|
||||||
|
|
||||||
return {
|
status = {
|
||||||
"system_healthy": InvenTree.status.check_system_health(),
|
'django_q_running': InvenTree.status.is_worker_running(),
|
||||||
|
'email_configured': InvenTree.status.is_email_configured(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
all_healthy = True
|
||||||
|
|
||||||
|
for k in status.keys():
|
||||||
|
if status[k] is not True:
|
||||||
|
all_healthy = False
|
||||||
|
|
||||||
|
status['system_healthy'] = all_healthy
|
||||||
|
|
||||||
|
status['up_to_date'] = InvenTree.version.isInvenTreeUpToDate()
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
def status_codes(request):
|
def status_codes(request):
|
||||||
"""
|
"""
|
||||||
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .validators import allowable_url_schemes
|
from .validators import allowable_url_schemes
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from django.forms.fields import URLField as FormURLField
|
from django.forms.fields import URLField as FormURLField
|
||||||
from django.db import models as models
|
from django.db import models as models
|
||||||
@ -42,6 +42,7 @@ class DatePickerFormField(forms.DateField):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
|
||||||
help_text = kwargs.get('help_text', _('Enter date'))
|
help_text = kwargs.get('help_text', _('Enter date'))
|
||||||
|
label = kwargs.get('label', None)
|
||||||
required = kwargs.get('required', False)
|
required = kwargs.get('required', False)
|
||||||
initial = kwargs.get('initial', None)
|
initial = kwargs.get('initial', None)
|
||||||
|
|
||||||
@ -56,7 +57,8 @@ class DatePickerFormField(forms.DateField):
|
|||||||
required=required,
|
required=required,
|
||||||
initial=initial,
|
initial=initial,
|
||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
widget=widget
|
widget=widget,
|
||||||
|
label=label
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ Helper forms which subclass Django forms to provide additional functionality
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django import forms
|
from django import forms
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Field
|
from crispy_forms.layout import Layout, Field
|
||||||
@ -123,6 +123,7 @@ class DeleteForm(forms.Form):
|
|||||||
confirm_delete = forms.BooleanField(
|
confirm_delete = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
initial=False,
|
initial=False,
|
||||||
|
label=_('Confirm delete'),
|
||||||
help_text=_('Confirm item deletion')
|
help_text=_('Confirm item deletion')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -155,6 +156,7 @@ class SetPasswordForm(HelperForm):
|
|||||||
required=True,
|
required=True,
|
||||||
initial='',
|
initial='',
|
||||||
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
|
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
|
||||||
|
label=_('Enter password'),
|
||||||
help_text=_('Enter new password'))
|
help_text=_('Enter new password'))
|
||||||
|
|
||||||
confirm_password = forms.CharField(max_length=100,
|
confirm_password = forms.CharField(max_length=100,
|
||||||
@ -162,6 +164,7 @@ class SetPasswordForm(HelperForm):
|
|||||||
required=True,
|
required=True,
|
||||||
initial='',
|
initial='',
|
||||||
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
|
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
|
||||||
|
label=_('Confirm password'),
|
||||||
help_text=_('Confirm new password'))
|
help_text=_('Confirm new password'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -13,7 +13,7 @@ from decimal import Decimal
|
|||||||
from wsgiref.util import FileWrapper
|
from wsgiref.util import FileWrapper
|
||||||
from django.http import StreamingHttpResponse
|
from django.http import StreamingHttpResponse
|
||||||
from django.core.exceptions import ValidationError, FieldError
|
from django.core.exceptions import ValidationError, FieldError
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
|
|
||||||
@ -382,17 +382,17 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
if a < b:
|
if a < b:
|
||||||
for n in range(a, b + 1):
|
for n in range(a, b + 1):
|
||||||
if n in numbers:
|
if n in numbers:
|
||||||
errors.append(_('Duplicate serial: {n}'.format(n=n)))
|
errors.append(_('Duplicate serial: {n}').format(n=n))
|
||||||
else:
|
else:
|
||||||
numbers.append(n)
|
numbers.append(n)
|
||||||
else:
|
else:
|
||||||
errors.append(_("Invalid group: {g}".format(g=group)))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
errors.append(_("Invalid group: {g}".format(g=group)))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
errors.append(_("Invalid group: {g}".format(g=group)))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -409,7 +409,7 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
|
|
||||||
# The number of extracted serial numbers must match the expected quantity
|
# The number of extracted serial numbers must match the expected quantity
|
||||||
if not expected_quantity == len(numbers):
|
if not expected_quantity == len(numbers):
|
||||||
raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})".format(s=len(numbers), q=expected_quantity))])
|
raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})").format(s=len(numbers), q=expected_quantity)])
|
||||||
|
|
||||||
return numbers
|
return numbers
|
||||||
|
|
||||||
|
42
InvenTree/InvenTree/management/commands/wait_for_db.py
Normal file
42
InvenTree/InvenTree/management/commands/wait_for_db.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Custom management command, wait for the database to be ready!
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.db.utils import OperationalError, ImproperlyConfigured
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""
|
||||||
|
django command to pause execution until the database is ready
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
|
||||||
|
self.stdout.write("Waiting for database...")
|
||||||
|
|
||||||
|
connected = False
|
||||||
|
|
||||||
|
while not connected:
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection.ensure_connection()
|
||||||
|
|
||||||
|
connected = True
|
||||||
|
|
||||||
|
except OperationalError as e:
|
||||||
|
self.stdout.write(f"Could not connect to database: {e}")
|
||||||
|
except ImproperlyConfigured as e:
|
||||||
|
self.stdout.write(f"Improperly configured: {e}")
|
||||||
|
else:
|
||||||
|
if not connection.is_usable():
|
||||||
|
self.stdout.write("Database configuration is not usable")
|
||||||
|
|
||||||
|
if connected:
|
||||||
|
self.stdout.write("Database connection sucessful!")
|
@ -8,7 +8,7 @@ import operator
|
|||||||
|
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class AuthRequiredMiddleware(object):
|
class AuthRequiredMiddleware(object):
|
||||||
@ -52,6 +52,10 @@ class AuthRequiredMiddleware(object):
|
|||||||
if request.path_info.startswith('/static/'):
|
if request.path_info.startswith('/static/'):
|
||||||
authorized = True
|
authorized = True
|
||||||
|
|
||||||
|
# Unauthorized users can access the login page
|
||||||
|
elif request.path_info.startswith('/accounts/'):
|
||||||
|
authorized = True
|
||||||
|
|
||||||
elif 'Authorization' in request.headers.keys():
|
elif 'Authorization' in request.headers.keys():
|
||||||
auth = request.headers['Authorization'].strip()
|
auth = request.headers['Authorization'].strip()
|
||||||
|
|
||||||
|
@ -56,19 +56,20 @@ class InvenTreeAttachment(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return os.path.basename(self.attachment.name)
|
return os.path.basename(self.attachment.name)
|
||||||
|
|
||||||
attachment = models.FileField(upload_to=rename_attachment,
|
attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'),
|
||||||
help_text=_('Select file to attach'))
|
help_text=_('Select file to attach'))
|
||||||
|
|
||||||
comment = models.CharField(blank=True, max_length=100, help_text=_('File comment'))
|
comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment'))
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
|
verbose_name=_('User'),
|
||||||
help_text=_('User'),
|
help_text=_('User'),
|
||||||
)
|
)
|
||||||
|
|
||||||
upload_date = models.DateField(auto_now_add=True, null=True, blank=True)
|
upload_date = models.DateField(auto_now_add=True, null=True, blank=True, verbose_name=_('upload date'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def basename(self):
|
def basename(self):
|
||||||
@ -103,12 +104,14 @@ class InvenTreeTree(MPTTModel):
|
|||||||
blank=False,
|
blank=False,
|
||||||
max_length=100,
|
max_length=100,
|
||||||
validators=[validate_tree_name],
|
validators=[validate_tree_name],
|
||||||
|
verbose_name=_("Name"),
|
||||||
help_text=_("Name"),
|
help_text=_("Name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
blank=True,
|
blank=True,
|
||||||
max_length=250,
|
max_length=250,
|
||||||
|
verbose_name=_("Description"),
|
||||||
help_text=_("Description (optional)")
|
help_text=_("Description (optional)")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -117,6 +120,7 @@ class InvenTreeTree(MPTTModel):
|
|||||||
on_delete=models.DO_NOTHING,
|
on_delete=models.DO_NOTHING,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
verbose_name=_("parent"),
|
||||||
related_name='children')
|
related_name='children')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -13,6 +13,9 @@ database setup in this file.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -46,14 +49,31 @@ def get_setting(environment_var, backup_val, default_value=None):
|
|||||||
return default_value
|
return default_value
|
||||||
|
|
||||||
|
|
||||||
|
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||||
|
TESTING = 'test' in sys.argv
|
||||||
|
|
||||||
# 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__)))
|
||||||
|
|
||||||
|
# Specify where the "config file" is located.
|
||||||
|
# By default, this is 'config.yaml'
|
||||||
|
|
||||||
|
cfg_filename = os.getenv('INVENTREE_CONFIG_FILE')
|
||||||
|
|
||||||
|
if cfg_filename:
|
||||||
|
cfg_filename = cfg_filename.strip()
|
||||||
|
cfg_filename = os.path.abspath(cfg_filename)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Config file is *not* specified - use the default
|
||||||
cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
|
cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
|
||||||
|
|
||||||
if not os.path.exists(cfg_filename):
|
if not os.path.exists(cfg_filename):
|
||||||
print("Error: config.yaml not found")
|
print("InvenTree configuration file 'config.yaml' not found - creating default file")
|
||||||
sys.exit(-1)
|
|
||||||
|
cfg_template = os.path.join(BASE_DIR, "config_template.yaml")
|
||||||
|
shutil.copyfile(cfg_template, cfg_filename)
|
||||||
|
print(f"Created config file {cfg_filename}")
|
||||||
|
|
||||||
with open(cfg_filename, 'r') as cfg:
|
with open(cfg_filename, 'r') as cfg:
|
||||||
CONFIG = yaml.safe_load(cfg)
|
CONFIG = yaml.safe_load(cfg)
|
||||||
@ -94,7 +114,18 @@ LOGGING = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Get a logger instance for this setup file
|
# Get a logger instance for this setup file
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
"""
|
||||||
|
Specify a secret key to be used by django.
|
||||||
|
|
||||||
|
Following options are tested, in descending order of preference:
|
||||||
|
|
||||||
|
A) Check for environment variable INVENTREE_SECRET_KEY => Use raw key data
|
||||||
|
B) Check for environment variable INVENTREE_SECRET_KEY_FILE => Load key data from file
|
||||||
|
C) Look for default key file "secret_key.txt"
|
||||||
|
d) Create "secret_key.txt" if it does not exist
|
||||||
|
"""
|
||||||
|
|
||||||
if os.getenv("INVENTREE_SECRET_KEY"):
|
if os.getenv("INVENTREE_SECRET_KEY"):
|
||||||
# Secret key passed in directly
|
# Secret key passed in directly
|
||||||
@ -105,15 +136,22 @@ else:
|
|||||||
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
|
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
|
||||||
|
|
||||||
if key_file:
|
if key_file:
|
||||||
if os.path.isfile(key_file):
|
key_file = os.path.abspath(key_file)
|
||||||
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY_FILE")
|
|
||||||
else:
|
|
||||||
logger.error(f"Secret key file {key_file} not found")
|
|
||||||
exit(-1)
|
|
||||||
else:
|
else:
|
||||||
# default secret key location
|
# default secret key location
|
||||||
key_file = os.path.join(BASE_DIR, "secret_key.txt")
|
key_file = os.path.join(BASE_DIR, "secret_key.txt")
|
||||||
logger.info(f"SECRET_KEY loaded from {key_file}")
|
key_file = os.path.abspath(key_file)
|
||||||
|
|
||||||
|
if not os.path.exists(key_file):
|
||||||
|
logger.info(f"Generating random key file at '{key_file}'")
|
||||||
|
# Create a random key file
|
||||||
|
with open(key_file, 'w') as f:
|
||||||
|
options = string.digits + string.ascii_letters + string.punctuation
|
||||||
|
key = ''.join([random.choice(options) for i in range(100)])
|
||||||
|
f.write(key)
|
||||||
|
|
||||||
|
logger.info(f"Loading SECRET_KEY from '{key_file}'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
SECRET_KEY = open(key_file, "r").read().strip()
|
SECRET_KEY = open(key_file, "r").read().strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -144,7 +182,7 @@ STATIC_URL = '/static/'
|
|||||||
STATIC_ROOT = os.path.abspath(
|
STATIC_ROOT = os.path.abspath(
|
||||||
get_setting(
|
get_setting(
|
||||||
'INVENTREE_STATIC_ROOT',
|
'INVENTREE_STATIC_ROOT',
|
||||||
CONFIG.get('static_root', os.path.join(BASE_DIR, 'static'))
|
CONFIG.get('static_root', '/home/inventree/static')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -162,7 +200,7 @@ MEDIA_URL = '/media/'
|
|||||||
MEDIA_ROOT = os.path.abspath(
|
MEDIA_ROOT = os.path.abspath(
|
||||||
get_setting(
|
get_setting(
|
||||||
'INVENTREE_MEDIA_ROOT',
|
'INVENTREE_MEDIA_ROOT',
|
||||||
CONFIG.get('media_root', os.path.join(BASE_DIR, 'media'))
|
CONFIG.get('media_root', '/home/inventree/data/media')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -194,6 +232,7 @@ INSTALLED_APPS = [
|
|||||||
'report.apps.ReportConfig',
|
'report.apps.ReportConfig',
|
||||||
'stock.apps.StockConfig',
|
'stock.apps.StockConfig',
|
||||||
'users.apps.UsersConfig',
|
'users.apps.UsersConfig',
|
||||||
|
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||||
|
|
||||||
# Third part add-ons
|
# Third part add-ons
|
||||||
'django_filters', # Extended filter functionality
|
'django_filters', # Extended filter functionality
|
||||||
@ -211,6 +250,7 @@ INSTALLED_APPS = [
|
|||||||
'djmoney', # django-money integration
|
'djmoney', # django-money integration
|
||||||
'djmoney.contrib.exchange', # django-money exchange rates
|
'djmoney.contrib.exchange', # django-money exchange rates
|
||||||
'error_report', # Error reporting in the admin interface
|
'error_report', # Error reporting in the admin interface
|
||||||
|
'django_q',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = CONFIG.get('middleware', [
|
MIDDLEWARE = CONFIG.get('middleware', [
|
||||||
@ -285,6 +325,18 @@ REST_FRAMEWORK = {
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
||||||
|
|
||||||
|
# django-q configuration
|
||||||
|
Q_CLUSTER = {
|
||||||
|
'name': 'InvenTree',
|
||||||
|
'workers': 4,
|
||||||
|
'timeout': 90,
|
||||||
|
'retry': 120,
|
||||||
|
'queue_limit': 50,
|
||||||
|
'bulk': 10,
|
||||||
|
'orm': 'default',
|
||||||
|
'sync': False,
|
||||||
|
}
|
||||||
|
|
||||||
# Markdownx configuration
|
# Markdownx configuration
|
||||||
# Ref: https://neutronx.github.io/django-markdownx/customization/
|
# Ref: https://neutronx.github.io/django-markdownx/customization/
|
||||||
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
|
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
|
||||||
@ -331,6 +383,9 @@ logger.info("Configuring database backend:")
|
|||||||
# Extract database configuration from the config.yaml file
|
# Extract database configuration from the config.yaml file
|
||||||
db_config = CONFIG.get('database', {})
|
db_config = CONFIG.get('database', {})
|
||||||
|
|
||||||
|
if not db_config:
|
||||||
|
db_config = {}
|
||||||
|
|
||||||
# Environment variables take preference over config file!
|
# Environment variables take preference over config file!
|
||||||
|
|
||||||
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
|
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
|
||||||
@ -350,7 +405,7 @@ reqiured_keys = ['ENGINE', 'NAME']
|
|||||||
|
|
||||||
for key in reqiured_keys:
|
for key in reqiured_keys:
|
||||||
if key not in db_config:
|
if key not in db_config:
|
||||||
error_msg = f'Missing required database configuration value {key} in config.yaml'
|
error_msg = f'Missing required database configuration value {key}'
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
|
||||||
print('Error: ' + error_msg)
|
print('Error: ' + error_msg)
|
||||||
@ -386,11 +441,6 @@ CACHES = {
|
|||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
},
|
},
|
||||||
'qr-code': {
|
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
|
||||||
'LOCATION': 'qr-code-cache',
|
|
||||||
'TIMEOUT': 3600
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
@ -445,16 +495,67 @@ CURRENCIES = CONFIG.get(
|
|||||||
# TODO - Allow live web-based backends in the future
|
# TODO - Allow live web-based backends in the future
|
||||||
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeManualExchangeBackend'
|
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeManualExchangeBackend'
|
||||||
|
|
||||||
|
# Extract email settings from the config file
|
||||||
|
email_config = CONFIG.get('email', {})
|
||||||
|
|
||||||
|
EMAIL_BACKEND = get_setting(
|
||||||
|
'django.core.mail.backends.smtp.EmailBackend',
|
||||||
|
email_config.get('backend', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Email backend settings
|
||||||
|
EMAIL_HOST = get_setting(
|
||||||
|
'INVENTREE_EMAIL_HOST',
|
||||||
|
email_config.get('host', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
EMAIL_PORT = get_setting(
|
||||||
|
'INVENTREE_EMAIL_PORT',
|
||||||
|
email_config.get('port', 25)
|
||||||
|
)
|
||||||
|
|
||||||
|
EMAIL_HOST_USER = get_setting(
|
||||||
|
'INVENTREE_EMAIL_USERNAME',
|
||||||
|
email_config.get('username', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
EMAIL_HOST_PASSWORD = get_setting(
|
||||||
|
'INVENTREE_EMAIL_PASSWORD',
|
||||||
|
email_config.get('password', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
EMAIL_SUBJECT_PREFIX = '[InvenTree] '
|
||||||
|
|
||||||
|
EMAIL_USE_LOCALTIME = False
|
||||||
|
|
||||||
|
EMAIL_USE_TLS = get_setting(
|
||||||
|
'INVENTREE_EMAIL_TLS',
|
||||||
|
email_config.get('tls', False),
|
||||||
|
)
|
||||||
|
|
||||||
|
EMAIL_USE_SSL = get_setting(
|
||||||
|
'INVENTREE_EMAIL_SSL',
|
||||||
|
email_config.get('ssl', False),
|
||||||
|
)
|
||||||
|
|
||||||
|
EMAIL_TIMEOUT = 60
|
||||||
|
|
||||||
LOCALE_PATHS = (
|
LOCALE_PATHS = (
|
||||||
os.path.join(BASE_DIR, 'locale/'),
|
os.path.join(BASE_DIR, 'locale/'),
|
||||||
)
|
)
|
||||||
|
|
||||||
TIME_ZONE = CONFIG.get('timezone', 'UTC')
|
TIME_ZONE = get_setting(
|
||||||
|
'INVENTREE_TIMEZONE',
|
||||||
|
CONFIG.get('timezone', 'UTC')
|
||||||
|
)
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
|
|
||||||
|
# Do not use native timezone support in "test" mode
|
||||||
|
# It generates a *lot* of cruft in the logs
|
||||||
|
if not TESTING:
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
DATE_INPUT_FORMATS = [
|
DATE_INPUT_FORMATS = [
|
||||||
|
@ -1,13 +1,73 @@
|
|||||||
"""
|
"""
|
||||||
Provides system status functionality checks.
|
Provides system status functionality checks.
|
||||||
"""
|
"""
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django_q.models import Success
|
||||||
|
from django_q.monitor import Stat
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
|
def is_worker_running(**kwargs):
|
||||||
|
"""
|
||||||
|
Return True if the background worker process is oprational
|
||||||
|
"""
|
||||||
|
|
||||||
|
clusters = Stat.get_all()
|
||||||
|
|
||||||
|
if len(clusters) > 0:
|
||||||
|
# TODO - Introspect on any cluster information
|
||||||
|
return True
|
||||||
|
|
||||||
|
"""
|
||||||
|
Sometimes Stat.get_all() returns [].
|
||||||
|
In this case we have the 'heartbeat' task running every 15 minutes.
|
||||||
|
Check to see if we have a result within the last 20 minutes
|
||||||
|
"""
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
past = now - timedelta(minutes=20)
|
||||||
|
|
||||||
|
results = Success.objects.filter(
|
||||||
|
func='InvenTree.tasks.heartbeat',
|
||||||
|
started__gte=past
|
||||||
|
)
|
||||||
|
|
||||||
|
# If any results are returned, then the background worker is running!
|
||||||
|
return results.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def is_email_configured():
|
||||||
|
"""
|
||||||
|
Check if email backend is configured.
|
||||||
|
|
||||||
|
NOTE: This does not check if the configuration is valid!
|
||||||
|
"""
|
||||||
|
|
||||||
|
configured = True
|
||||||
|
|
||||||
|
if not settings.EMAIL_HOST:
|
||||||
|
logger.warning("EMAIL_HOST is not configured")
|
||||||
|
configured = False
|
||||||
|
|
||||||
|
if not settings.EMAIL_HOST_USER:
|
||||||
|
logger.warning("EMAIL_HOST_USER is not configured")
|
||||||
|
configured = False
|
||||||
|
|
||||||
|
if not settings.EMAIL_HOST_PASSWORD:
|
||||||
|
logger.warning("EMAIL_HOST_PASSWORD is not configured")
|
||||||
|
configured = False
|
||||||
|
|
||||||
|
return configured
|
||||||
|
|
||||||
|
|
||||||
def check_system_health(**kwargs):
|
def check_system_health(**kwargs):
|
||||||
@ -19,21 +79,15 @@ def check_system_health(**kwargs):
|
|||||||
|
|
||||||
result = True
|
result = True
|
||||||
|
|
||||||
if not check_celery_worker(**kwargs):
|
if not is_worker_running(**kwargs):
|
||||||
result = False
|
result = False
|
||||||
logger.warning(_("Celery worker check failed"))
|
logger.warning(_("Background worker check failed"))
|
||||||
|
|
||||||
|
if not is_email_configured():
|
||||||
|
result = False
|
||||||
|
logger.warning(_("Email backend not configured"))
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
logger.warning(_("InvenTree system health checks failed"))
|
logger.warning(_("InvenTree system health checks failed"))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def check_celery_worker(**kwargs):
|
|
||||||
"""
|
|
||||||
Check that a celery worker is running.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO - Checks that the configured celery worker thing is running
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class StatusCode:
|
class StatusCode:
|
||||||
|
178
InvenTree/InvenTree/tasks.py
Normal file
178
InvenTree/InvenTree/tasks.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_task(taskname, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a scheduled task.
|
||||||
|
If the task has already been scheduled, ignore!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If unspecified, repeat indefinitely
|
||||||
|
repeats = kwargs.pop('repeats', -1)
|
||||||
|
kwargs['repeats'] = repeats
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django_q.models import Schedule
|
||||||
|
except (AppRegistryNotReady):
|
||||||
|
logger.warning("Could not start background tasks - App registry not ready")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If this task is already scheduled, don't schedule it again
|
||||||
|
# Instead, update the scheduling parameters
|
||||||
|
if Schedule.objects.filter(func=taskname).exists():
|
||||||
|
logger.info(f"Scheduled task '{taskname}' already exists - updating!")
|
||||||
|
|
||||||
|
Schedule.objects.filter(func=taskname).update(**kwargs)
|
||||||
|
else:
|
||||||
|
logger.info(f"Creating scheduled task '{taskname}'")
|
||||||
|
|
||||||
|
Schedule.objects.create(
|
||||||
|
name=taskname,
|
||||||
|
func=taskname,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
# Required if the DB is not ready yet
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def offload_task(taskname, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create an AsyncTask.
|
||||||
|
This is different to a 'scheduled' task,
|
||||||
|
in that it only runs once!
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django_q.tasks import AsyncTask
|
||||||
|
except (AppRegistryNotReady):
|
||||||
|
logger.warning("Could not offload task - app registry not ready")
|
||||||
|
return
|
||||||
|
|
||||||
|
task = AsyncTask(taskname, *args, **kwargs)
|
||||||
|
|
||||||
|
task.run()
|
||||||
|
|
||||||
|
|
||||||
|
def heartbeat():
|
||||||
|
"""
|
||||||
|
Simple task which runs at 5 minute intervals,
|
||||||
|
so we can determine that the background worker
|
||||||
|
is actually running.
|
||||||
|
|
||||||
|
(There is probably a less "hacky" way of achieving this)?
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django_q.models import Success
|
||||||
|
logger.warning("Could not perform heartbeat task - App registry not ready")
|
||||||
|
except AppRegistryNotReady:
|
||||||
|
return
|
||||||
|
|
||||||
|
threshold = datetime.now() - timedelta(minutes=30)
|
||||||
|
|
||||||
|
# Delete heartbeat results more than half an hour old,
|
||||||
|
# otherwise they just create extra noise
|
||||||
|
heartbeats = Success.objects.filter(
|
||||||
|
func='InvenTree.tasks.heartbeat',
|
||||||
|
started__lte=threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
heartbeats.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_successful_tasks():
|
||||||
|
"""
|
||||||
|
Delete successful task logs
|
||||||
|
which are more than a month old.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django_q.models import Success
|
||||||
|
except AppRegistryNotReady:
|
||||||
|
logger.warning("Could not perform 'delete_successful_tasks' - App registry not ready")
|
||||||
|
return
|
||||||
|
|
||||||
|
threshold = datetime.now() - timedelta(days=30)
|
||||||
|
|
||||||
|
results = Success.objects.filter(
|
||||||
|
started__lte=threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
results.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_updates():
|
||||||
|
"""
|
||||||
|
Check if there is an update for InvenTree
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import common.models
|
||||||
|
except AppRegistryNotReady:
|
||||||
|
# Apps not yet loaded!
|
||||||
|
return
|
||||||
|
|
||||||
|
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
|
||||||
|
|
||||||
|
if not response.status_code == 200:
|
||||||
|
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
|
||||||
|
|
||||||
|
data = json.loads(response.text)
|
||||||
|
|
||||||
|
tag = data.get('tag_name', None)
|
||||||
|
|
||||||
|
if not tag:
|
||||||
|
raise ValueError("'tag_name' missing from GitHub response")
|
||||||
|
|
||||||
|
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
|
||||||
|
|
||||||
|
if not len(match.groups()) == 3:
|
||||||
|
logger.warning(f"Version '{tag}' did not match expected pattern")
|
||||||
|
return
|
||||||
|
|
||||||
|
latest_version = [int(x) for x in match.groups()]
|
||||||
|
|
||||||
|
if not len(latest_version) == 3:
|
||||||
|
raise ValueError(f"Version '{tag}' is not correct format")
|
||||||
|
|
||||||
|
logger.info(f"Latest InvenTree version: '{tag}'")
|
||||||
|
|
||||||
|
# Save the version to the database
|
||||||
|
common.models.InvenTreeSetting.set_setting(
|
||||||
|
'INVENTREE_LATEST_VERSION',
|
||||||
|
tag,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(subject, body, recipients, from_email=None):
|
||||||
|
"""
|
||||||
|
Send an email with the specified subject and body,
|
||||||
|
to the specified recipients list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if type(recipients) == str:
|
||||||
|
recipients = [recipients]
|
||||||
|
|
||||||
|
offload_task(
|
||||||
|
'django.core.mail.send_mail',
|
||||||
|
subject, body,
|
||||||
|
from_email,
|
||||||
|
recipients,
|
||||||
|
)
|
43
InvenTree/InvenTree/test_tasks.py
Normal file
43
InvenTree/InvenTree/test_tasks.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for task management
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django_q.models import Schedule
|
||||||
|
|
||||||
|
import InvenTree.tasks
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledTaskTests(TestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for scheduled tasks
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_tasks(self, name):
|
||||||
|
|
||||||
|
return Schedule.objects.filter(func=name)
|
||||||
|
|
||||||
|
def test_add_task(self):
|
||||||
|
"""
|
||||||
|
Ensure that duplicate tasks cannot be added.
|
||||||
|
"""
|
||||||
|
|
||||||
|
task = 'InvenTree.tasks.heartbeat'
|
||||||
|
|
||||||
|
self.assertEqual(self.get_tasks(task).count(), 0)
|
||||||
|
|
||||||
|
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=10)
|
||||||
|
|
||||||
|
self.assertEqual(self.get_tasks(task).count(), 1)
|
||||||
|
|
||||||
|
t = Schedule.objects.get(func=task)
|
||||||
|
|
||||||
|
self.assertEqual(t.minutes, 10)
|
||||||
|
|
||||||
|
# Attempt to schedule the same task again
|
||||||
|
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5)
|
||||||
|
self.assertEqual(self.get_tasks(task).count(), 1)
|
||||||
|
|
||||||
|
# But the 'minutes' should have been updated
|
||||||
|
t = Schedule.objects.get(func=task)
|
||||||
|
self.assertEqual(t.minutes, 5)
|
@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError
|
|||||||
|
|
||||||
from .validators import validate_overage, validate_part_name
|
from .validators import validate_overage, validate_part_name
|
||||||
from . import helpers
|
from . import helpers
|
||||||
|
from . import version
|
||||||
|
|
||||||
from mptt.exceptions import InvalidMove
|
from mptt.exceptions import InvalidMove
|
||||||
|
|
||||||
@ -269,3 +270,33 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("10, a, 7-70j", 4)
|
e("10, a, 7-70j", 4)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersionNumber(TestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for version number functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_tuple(self):
|
||||||
|
|
||||||
|
v = version.inventreeVersionTuple()
|
||||||
|
self.assertEqual(len(v), 3)
|
||||||
|
|
||||||
|
s = '.'.join([str(i) for i in v])
|
||||||
|
|
||||||
|
self.assertTrue(s in version.inventreeVersion())
|
||||||
|
|
||||||
|
def test_comparison(self):
|
||||||
|
"""
|
||||||
|
Test direct comparison of version numbers
|
||||||
|
"""
|
||||||
|
|
||||||
|
v_a = version.inventreeVersionTuple('1.2.0')
|
||||||
|
v_b = version.inventreeVersionTuple('1.2.3')
|
||||||
|
v_c = version.inventreeVersionTuple('1.2.4')
|
||||||
|
v_d = version.inventreeVersionTuple('2.0.0')
|
||||||
|
|
||||||
|
self.assertTrue(v_b > v_a)
|
||||||
|
self.assertTrue(v_c > v_b)
|
||||||
|
self.assertTrue(v_d > v_c)
|
||||||
|
self.assertTrue(v_d > v_a)
|
||||||
|
@ -110,6 +110,7 @@ dynamic_javascript_urls = [
|
|||||||
url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
|
url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
|
||||||
url(r'^tables.js', DynamicJsView.as_view(template_name='js/tables.js'), name='tables.js'),
|
url(r'^tables.js', DynamicJsView.as_view(template_name='js/tables.js'), name='tables.js'),
|
||||||
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
|
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
|
||||||
|
url(r'^filters.js', DynamicJsView.as_view(template_name='js/filters.js'), name='filters.js'),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -132,7 +133,7 @@ urlpatterns = [
|
|||||||
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
|
|
||||||
url(r'^login/?', auth_views.LoginView.as_view(), name='login'),
|
url(r'^login/?', auth_views.LoginView.as_view(), name='login'),
|
||||||
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'),
|
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logged_out.html'), name='logout'),
|
||||||
|
|
||||||
url(r'^settings/', include(settings_urls)),
|
url(r'^settings/', include(settings_urls)),
|
||||||
|
|
||||||
@ -142,6 +143,7 @@ urlpatterns = [
|
|||||||
url(r'^admin/error_log/', include('error_report.urls')),
|
url(r'^admin/error_log/', include('error_report.urls')),
|
||||||
url(r'^admin/shell/', include('django_admin_shell.urls')),
|
url(r'^admin/shell/', include('django_admin_shell.urls')),
|
||||||
url(r'^admin/', admin.site.urls, name='inventree-admin'),
|
url(r'^admin/', admin.site.urls, name='inventree-admin'),
|
||||||
|
url(r'accounts/', include('django.contrib.auth.urls')),
|
||||||
|
|
||||||
url(r'^index/', IndexView.as_view(), name='index'),
|
url(r'^index/', IndexView.as_view(), name='index'),
|
||||||
url(r'^search/', SearchView.as_view(), name='search'),
|
url(r'^search/', SearchView.as_view(), name='search'),
|
||||||
|
@ -60,7 +60,7 @@ def validate_part_ipn(value):
|
|||||||
match = re.search(pattern, value)
|
match = re.search(pattern, value)
|
||||||
|
|
||||||
if match is None:
|
if match is None:
|
||||||
raise ValidationError(_('IPN must match regex pattern') + " '{pat}'".format(pat=pattern))
|
raise ValidationError(_('IPN must match regex pattern {pat}').format(pat=pattern))
|
||||||
|
|
||||||
|
|
||||||
def validate_build_order_reference(value):
|
def validate_build_order_reference(value):
|
||||||
|
@ -4,10 +4,11 @@ Provides information on the current InvenTree version
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import django
|
import django
|
||||||
|
import re
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.1.8 pre"
|
INVENTREE_SW_VERSION = "0.2.1 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
|
||||||
@ -23,6 +24,38 @@ def inventreeVersion():
|
|||||||
return INVENTREE_SW_VERSION
|
return INVENTREE_SW_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
def inventreeVersionTuple(version=None):
|
||||||
|
""" Return the InvenTree version string as (maj, min, sub) tuple """
|
||||||
|
|
||||||
|
if version is None:
|
||||||
|
version = INVENTREE_SW_VERSION
|
||||||
|
|
||||||
|
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", str(version))
|
||||||
|
|
||||||
|
return [int(g) for g in match.groups()]
|
||||||
|
|
||||||
|
|
||||||
|
def isInvenTreeUpToDate():
|
||||||
|
"""
|
||||||
|
Test if the InvenTree instance is "up to date" with the latest version.
|
||||||
|
|
||||||
|
A background task periodically queries GitHub for latest version,
|
||||||
|
and stores it to the database as INVENTREE_LATEST_VERSION
|
||||||
|
"""
|
||||||
|
|
||||||
|
latest = common.models.InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION', None)
|
||||||
|
|
||||||
|
# No record for "latest" version - we must assume we are up to date!
|
||||||
|
if not latest:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Extract "tuple" version (Python can directly compare version tuples)
|
||||||
|
latest_version = inventreeVersionTuple(latest)
|
||||||
|
inventree_version = inventreeVersionTuple()
|
||||||
|
|
||||||
|
return inventree_version >= latest_version
|
||||||
|
|
||||||
|
|
||||||
def inventreeApiVersion():
|
def inventreeApiVersion():
|
||||||
return INVENTREE_API_VERSION
|
return INVENTREE_API_VERSION
|
||||||
|
|
||||||
@ -37,7 +70,7 @@ def inventreeCommitHash():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
||||||
except FileNotFoundError:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -47,5 +80,5 @@ def inventreeCommitDate():
|
|||||||
try:
|
try:
|
||||||
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
|
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
|
||||||
return d.split(' ')[0]
|
return d.split(' ')[0]
|
||||||
except FileNotFoundError:
|
except:
|
||||||
return None
|
return None
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
@ -12,7 +12,7 @@ from stock.serializers import StockItemSerializer, LocationSerializer
|
|||||||
from part.serializers import PartSerializer
|
from part.serializers import PartSerializer
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def hash_barcode(barcode_data):
|
def hash_barcode(barcode_data):
|
||||||
|
@ -5,7 +5,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 as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
@ -36,11 +36,13 @@ class EditBuildForm(HelperForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
target_date = DatePickerFormField(
|
target_date = DatePickerFormField(
|
||||||
|
label=_('Target Date'),
|
||||||
help_text=_('Target date for build completion. Build will be overdue after this date.')
|
help_text=_('Target date for build completion. Build will be overdue after this date.')
|
||||||
)
|
)
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(
|
quantity = RoundingDecimalFormField(
|
||||||
max_digits=10, decimal_places=5,
|
max_digits=10, decimal_places=5,
|
||||||
|
label=_('Quantity'),
|
||||||
help_text=_('Number of items to build')
|
help_text=_('Number of items to build')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -87,7 +89,7 @@ class BuildOutputCreateForm(HelperForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
serial_numbers = forms.CharField(
|
serial_numbers = forms.CharField(
|
||||||
label=_('Serial numbers'),
|
label=_('Serial Numbers'),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('Enter serial numbers for build outputs'),
|
help_text=_('Enter serial numbers for build outputs'),
|
||||||
)
|
)
|
||||||
@ -115,6 +117,7 @@ class BuildOutputDeleteForm(HelperForm):
|
|||||||
|
|
||||||
confirm = forms.BooleanField(
|
confirm = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
|
label=_('Confirm'),
|
||||||
help_text=_('Confirm deletion of build output')
|
help_text=_('Confirm deletion of build output')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -136,7 +139,7 @@ class UnallocateBuildForm(HelperForm):
|
|||||||
Form for auto-de-allocation of stock from a build
|
Form for auto-de-allocation of stock from a build
|
||||||
"""
|
"""
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=False, help_text=_('Confirm unallocation of stock'))
|
confirm = forms.BooleanField(required=False, label=_('Confirm'), help_text=_('Confirm unallocation of stock'))
|
||||||
|
|
||||||
output_id = forms.IntegerField(
|
output_id = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -160,7 +163,7 @@ class UnallocateBuildForm(HelperForm):
|
|||||||
class AutoAllocateForm(HelperForm):
|
class AutoAllocateForm(HelperForm):
|
||||||
""" Form for auto-allocation of stock to a build """
|
""" Form for auto-allocation of stock to a build """
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=True, 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
|
# Keep track of which build output we are interested in
|
||||||
output = forms.ModelChoiceField(
|
output = forms.ModelChoiceField(
|
||||||
@ -207,15 +210,17 @@ class CompleteBuildOutputForm(HelperForm):
|
|||||||
|
|
||||||
location = forms.ModelChoiceField(
|
location = forms.ModelChoiceField(
|
||||||
queryset=StockLocation.objects.all(),
|
queryset=StockLocation.objects.all(),
|
||||||
|
label=_('Location'),
|
||||||
help_text=_('Location of completed parts'),
|
help_text=_('Location of completed parts'),
|
||||||
)
|
)
|
||||||
|
|
||||||
confirm_incomplete = forms.BooleanField(
|
confirm_incomplete = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
|
label=_('Confirm incomplete'),
|
||||||
help_text=_("Confirm completion with incomplete stock allocation")
|
help_text=_("Confirm completion with incomplete stock allocation")
|
||||||
)
|
)
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=True, help_text=_('Confirm build completion'))
|
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm build completion'))
|
||||||
|
|
||||||
output = forms.ModelChoiceField(
|
output = forms.ModelChoiceField(
|
||||||
queryset=StockItem.objects.all(), # Queryset is narrowed in the view
|
queryset=StockItem.objects.all(), # Queryset is narrowed in the view
|
||||||
@ -235,7 +240,7 @@ class CompleteBuildOutputForm(HelperForm):
|
|||||||
class CancelBuildForm(HelperForm):
|
class CancelBuildForm(HelperForm):
|
||||||
""" Form for cancelling a build """
|
""" Form for cancelling a build """
|
||||||
|
|
||||||
confirm_cancel = forms.BooleanField(required=False, help_text=_('Confirm build cancellation'))
|
confirm_cancel = forms.BooleanField(required=False, label=_('Confirm cancel'), help_text=_('Confirm build cancellation'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Build
|
model = Build
|
||||||
@ -249,7 +254,7 @@ class EditBuildItemForm(HelperForm):
|
|||||||
Form for creating (or editing) a BuildItem object.
|
Form for creating (or editing) a BuildItem object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, help_text=_('Select quantity of stock to allocate'))
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'), help_text=_('Select quantity of stock to allocate'))
|
||||||
|
|
||||||
part_id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
part_id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||||
|
|
||||||
|
85
InvenTree/build/migrations/0027_auto_20210404_2016.py
Normal file
85
InvenTree/build/migrations/0027_auto_20210404_2016.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-04-04 20:16
|
||||||
|
|
||||||
|
import InvenTree.models
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0058_stockitem_packaging'),
|
||||||
|
('users', '0005_owner_model'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('build', '0026_auto_20210216_1539'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='build',
|
||||||
|
name='completed_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_completed', to=settings.AUTH_USER_MODEL, verbose_name='completed by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='build',
|
||||||
|
name='completion_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Completion Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='build',
|
||||||
|
name='creation_date',
|
||||||
|
field=models.DateField(auto_now_add=True, verbose_name='Creation Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='build',
|
||||||
|
name='issued_by',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User who issued this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_issued', to=settings.AUTH_USER_MODEL, verbose_name='Issued by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='build',
|
||||||
|
name='responsible',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User responsible for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_responsible', to='users.Owner', verbose_name='Responsible'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='builditem',
|
||||||
|
name='build',
|
||||||
|
field=models.ForeignKey(help_text='Build to allocate parts', on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build', verbose_name='Build'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='builditem',
|
||||||
|
name='install_into',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.StockItem', verbose_name='Install into'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='builditem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=5, default=1, help_text='Stock quantity to allocate to build', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='builditem',
|
||||||
|
name='stock_item',
|
||||||
|
field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem', verbose_name='Stock Item'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='buildorderattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='buildorderattachment',
|
||||||
|
name='comment',
|
||||||
|
field=models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='buildorderattachment',
|
||||||
|
name='upload_date',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True, verbose_name='upload date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='buildorderattachment',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
]
|
@ -9,7 +9,7 @@ import os
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -216,7 +216,7 @@ class Build(MPTTModel):
|
|||||||
help_text=_('Batch code for this build output')
|
help_text=_('Batch code for this build output')
|
||||||
)
|
)
|
||||||
|
|
||||||
creation_date = models.DateField(auto_now_add=True, editable=False)
|
creation_date = models.DateField(auto_now_add=True, editable=False, verbose_name=_('Creation Date'))
|
||||||
|
|
||||||
target_date = models.DateField(
|
target_date = models.DateField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@ -224,12 +224,13 @@ class Build(MPTTModel):
|
|||||||
help_text=_('Target date for build completion. Build will be overdue after this date.')
|
help_text=_('Target date for build completion. Build will be overdue after this date.')
|
||||||
)
|
)
|
||||||
|
|
||||||
completion_date = models.DateField(null=True, blank=True)
|
completion_date = models.DateField(null=True, blank=True, verbose_name=_('Completion Date'))
|
||||||
|
|
||||||
completed_by = models.ForeignKey(
|
completed_by = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
|
verbose_name=_('completed by'),
|
||||||
related_name='builds_completed'
|
related_name='builds_completed'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -237,6 +238,7 @@ class Build(MPTTModel):
|
|||||||
User,
|
User,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Issued by'),
|
||||||
help_text=_('User who issued this build order'),
|
help_text=_('User who issued this build order'),
|
||||||
related_name='builds_issued',
|
related_name='builds_issued',
|
||||||
)
|
)
|
||||||
@ -245,6 +247,7 @@ class Build(MPTTModel):
|
|||||||
UserModels.Owner,
|
UserModels.Owner,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Responsible'),
|
||||||
help_text=_('User responsible for this build order'),
|
help_text=_('User responsible for this build order'),
|
||||||
related_name='builds_responsible',
|
related_name='builds_responsible',
|
||||||
)
|
)
|
||||||
@ -1017,14 +1020,14 @@ class BuildItem(models.Model):
|
|||||||
try:
|
try:
|
||||||
# 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)]
|
||||||
|
|
||||||
# Allocated quantity cannot exceed available stock quantity
|
# Allocated quantity cannot exceed available stock quantity
|
||||||
if self.quantity > self.stock_item.quantity:
|
if self.quantity > self.stock_item.quantity:
|
||||||
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})".format(
|
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format(
|
||||||
n=normalize(self.quantity),
|
n=normalize(self.quantity),
|
||||||
q=normalize(self.stock_item.quantity)
|
q=normalize(self.stock_item.quantity)
|
||||||
))]
|
)]
|
||||||
|
|
||||||
# Allocated quantity cannot cause the stock item to be over-allocated
|
# Allocated quantity cannot cause the stock item to be over-allocated
|
||||||
if self.stock_item.quantity - self.stock_item.allocation_count() + self.quantity < self.quantity:
|
if self.stock_item.quantity - self.stock_item.allocation_count() + self.quantity < self.quantity:
|
||||||
@ -1076,6 +1079,7 @@ class BuildItem(models.Model):
|
|||||||
Build,
|
Build,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='allocated_stock',
|
related_name='allocated_stock',
|
||||||
|
verbose_name=_('Build'),
|
||||||
help_text=_('Build to allocate parts')
|
help_text=_('Build to allocate parts')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1083,6 +1087,7 @@ class BuildItem(models.Model):
|
|||||||
'stock.StockItem',
|
'stock.StockItem',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='allocations',
|
related_name='allocations',
|
||||||
|
verbose_name=_('Stock Item'),
|
||||||
help_text=_('Source stock item'),
|
help_text=_('Source stock item'),
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
'sales_order': None,
|
'sales_order': None,
|
||||||
@ -1095,6 +1100,7 @@ class BuildItem(models.Model):
|
|||||||
max_digits=15,
|
max_digits=15,
|
||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(0)],
|
validators=[MinValueValidator(0)],
|
||||||
|
verbose_name=_('Quantity'),
|
||||||
help_text=_('Stock quantity to allocate to build')
|
help_text=_('Stock quantity to allocate to build')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1103,6 +1109,7 @@ class BuildItem(models.Model):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
related_name='items_to_install',
|
related_name='items_to_install',
|
||||||
|
verbose_name=_('Install into'),
|
||||||
help_text=_('Destination stock item'),
|
help_text=_('Destination stock item'),
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
'is_building': True,
|
'is_building': True,
|
||||||
|
@ -164,7 +164,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
launchModalForm("{% url 'build-cancel' build.id %}",
|
launchModalForm("{% url 'build-cancel' build.id %}",
|
||||||
{
|
{
|
||||||
reload: true,
|
reload: true,
|
||||||
submit_text: "Cancel Build",
|
submit_text: '{% trans "Cancel Build" %}',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
"{% url 'build-complete' build.id %}",
|
"{% url 'build-complete' build.id %}",
|
||||||
{
|
{
|
||||||
reload: true,
|
reload: true,
|
||||||
submit_text: "Complete Build",
|
submit_text: '{% trans "Complete Build" %}',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -130,6 +130,7 @@ InvenTree | {% trans "Build Orders" %}
|
|||||||
initialView: 'dayGridMonth',
|
initialView: 'dayGridMonth',
|
||||||
nowIndicator: true,
|
nowIndicator: true,
|
||||||
aspectRatio: 2.5,
|
aspectRatio: 2.5,
|
||||||
|
locale: '{{request.LANGUAGE_CODE}}',
|
||||||
datesSet: function() {
|
datesSet: function() {
|
||||||
loadOrderEvents(calendar);
|
loadOrderEvents(calendar);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ Django views 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 as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.views.generic import DetailView, ListView, UpdateView
|
from django.views.generic import DetailView, ListView, UpdateView
|
||||||
from django.forms import HiddenInput
|
from django.forms import HiddenInput
|
||||||
|
@ -17,7 +17,7 @@ from djmoney.models.fields import MoneyField
|
|||||||
from djmoney.contrib.exchange.models import convert_money
|
from djmoney.contrib.exchange.models import convert_money
|
||||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator, URLValidator
|
from django.core.validators import MinValueValidator, URLValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
@ -500,7 +500,7 @@ class InvenTreeSetting(models.Model):
|
|||||||
create: If True, create a new setting if the specified key does not exist.
|
create: If True, create a new setting if the specified key does not exist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not user.is_staff:
|
if user is not None and not user.is_staff:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -5,7 +5,7 @@ Django views for interacting with common models
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.forms import CheckboxInput, Select
|
from django.forms import CheckboxInput, Select
|
||||||
|
|
||||||
from InvenTree.views import AjaxUpdateView
|
from InvenTree.views import AjaxUpdateView
|
||||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
from PIL import UnidentifiedImageError
|
from PIL import UnidentifiedImageError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class CompanyConfig(AppConfig):
|
class CompanyConfig(AppConfig):
|
||||||
|
@ -8,7 +8,7 @@ from __future__ import unicode_literals
|
|||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from InvenTree.fields import RoundingDecimalFormField
|
from InvenTree.fields import RoundingDecimalFormField
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
import django.forms
|
import django.forms
|
||||||
|
|
||||||
import djmoney.settings
|
import djmoney.settings
|
||||||
@ -34,6 +34,7 @@ class EditCompanyForm(HelperForm):
|
|||||||
|
|
||||||
currency = django.forms.ChoiceField(
|
currency = django.forms.ChoiceField(
|
||||||
required=False,
|
required=False,
|
||||||
|
label=_('Currency'),
|
||||||
help_text=_('Default currency used for this company'),
|
help_text=_('Default currency used for this company'),
|
||||||
choices=[('', '----------')] + djmoney.settings.CURRENCY_CHOICES,
|
choices=[('', '----------')] + djmoney.settings.CURRENCY_CHOICES,
|
||||||
initial=common.settings.currency_code_default,
|
initial=common.settings.currency_code_default,
|
||||||
|
69
InvenTree/company/migrations/0032_auto_20210403_1837.py
Normal file
69
InvenTree/company/migrations/0032_auto_20210403_1837.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-04-03 18:37
|
||||||
|
|
||||||
|
import InvenTree.fields
|
||||||
|
import company.models
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import markdownx.models
|
||||||
|
import stdimage.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('company', '0031_auto_20210103_2215'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='company',
|
||||||
|
name='image',
|
||||||
|
field=stdimage.models.StdImageField(blank=True, null=True, upload_to=company.models.rename_company_image, verbose_name='Image'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='company',
|
||||||
|
name='is_customer',
|
||||||
|
field=models.BooleanField(default=False, help_text='Do you sell items to this company?', verbose_name='is customer'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='company',
|
||||||
|
name='is_manufacturer',
|
||||||
|
field=models.BooleanField(default=False, help_text='Does this company manufacture parts?', verbose_name='is manufacturer'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='company',
|
||||||
|
name='is_supplier',
|
||||||
|
field=models.BooleanField(default=True, help_text='Do you purchase items from this company?', verbose_name='is supplier'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='company',
|
||||||
|
name='link',
|
||||||
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external company information', verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='company',
|
||||||
|
name='notes',
|
||||||
|
field=markdownx.models.MarkdownxField(blank=True, verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplierpart',
|
||||||
|
name='base_cost',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, help_text='Minimum charge (e.g. stocking fee)', max_digits=10, validators=[django.core.validators.MinValueValidator(0)], verbose_name='base cost'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplierpart',
|
||||||
|
name='multiple',
|
||||||
|
field=models.PositiveIntegerField(default=1, help_text='Order multiple', validators=[django.core.validators.MinValueValidator(1)], verbose_name='multiple'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplierpart',
|
||||||
|
name='packaging',
|
||||||
|
field=models.CharField(blank=True, help_text='Part packaging', max_length=50, null=True, verbose_name='Packaging'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplierpricebreak',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pricebreaks', to='company.SupplierPart', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/company/migrations/0033_auto_20210410_1528.py
Normal file
18
InvenTree/company/migrations/0033_auto_20210410_1528.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-04-10 05:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('company', '0032_auto_20210403_1837'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='company',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, help_text='Description of the company', max_length=500, verbose_name='Company description'),
|
||||||
|
),
|
||||||
|
]
|
@ -9,7 +9,7 @@ import os
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum, Q, UniqueConstraint
|
from django.db.models import Sum, Q, UniqueConstraint
|
||||||
@ -95,7 +95,12 @@ class Company(models.Model):
|
|||||||
help_text=_('Company name'),
|
help_text=_('Company name'),
|
||||||
verbose_name=_('Company name'))
|
verbose_name=_('Company name'))
|
||||||
|
|
||||||
description = models.CharField(max_length=500, verbose_name=_('Company description'), help_text=_('Description of the company'))
|
description = models.CharField(
|
||||||
|
max_length=500,
|
||||||
|
verbose_name=_('Company description'),
|
||||||
|
help_text=_('Description of the company'),
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
website = models.URLField(blank=True, verbose_name=_('Website'), help_text=_('Company website URL'))
|
website = models.URLField(blank=True, verbose_name=_('Website'), help_text=_('Company website URL'))
|
||||||
|
|
||||||
@ -114,7 +119,7 @@ class Company(models.Model):
|
|||||||
verbose_name=_('Contact'),
|
verbose_name=_('Contact'),
|
||||||
blank=True, help_text=_('Point of contact'))
|
blank=True, help_text=_('Point of contact'))
|
||||||
|
|
||||||
link = InvenTreeURLField(blank=True, help_text=_('Link to external company information'))
|
link = InvenTreeURLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external company information'))
|
||||||
|
|
||||||
image = StdImageField(
|
image = StdImageField(
|
||||||
upload_to=rename_company_image,
|
upload_to=rename_company_image,
|
||||||
@ -122,15 +127,16 @@ class Company(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
variations={'thumbnail': (128, 128)},
|
variations={'thumbnail': (128, 128)},
|
||||||
delete_orphans=True,
|
delete_orphans=True,
|
||||||
|
verbose_name=_('Image'),
|
||||||
)
|
)
|
||||||
|
|
||||||
notes = MarkdownxField(blank=True)
|
notes = MarkdownxField(blank=True, verbose_name=_('Notes'))
|
||||||
|
|
||||||
is_customer = models.BooleanField(default=False, help_text=_('Do you sell items to this company?'))
|
is_customer = models.BooleanField(default=False, verbose_name=_('is customer'), help_text=_('Do you sell items to this company?'))
|
||||||
|
|
||||||
is_supplier = models.BooleanField(default=True, help_text=_('Do you purchase items from this company?'))
|
is_supplier = models.BooleanField(default=True, verbose_name=_('is supplier'), help_text=_('Do you purchase items from this company?'))
|
||||||
|
|
||||||
is_manufacturer = models.BooleanField(default=False, help_text=_('Does this company manufacture parts?'))
|
is_manufacturer = models.BooleanField(default=False, verbose_name=_('is manufacturer'), help_text=_('Does this company manufacture parts?'))
|
||||||
|
|
||||||
currency = models.CharField(
|
currency = models.CharField(
|
||||||
max_length=3,
|
max_length=3,
|
||||||
@ -366,11 +372,11 @@ class SupplierPart(models.Model):
|
|||||||
help_text=_('Notes')
|
help_text=_('Notes')
|
||||||
)
|
)
|
||||||
|
|
||||||
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text=_('Minimum charge (e.g. stocking fee)'))
|
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], verbose_name=_('base cost'), help_text=_('Minimum charge (e.g. stocking fee)'))
|
||||||
|
|
||||||
packaging = models.CharField(max_length=50, blank=True, null=True, help_text=_('Part packaging'))
|
packaging = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('Packaging'), help_text=_('Part packaging'))
|
||||||
|
|
||||||
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text=('Order multiple'))
|
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name=_('multiple'), help_text=_('Order multiple'))
|
||||||
|
|
||||||
# TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
|
# TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
|
||||||
# lead_time = models.DurationField(blank=True, null=True)
|
# lead_time = models.DurationField(blank=True, null=True)
|
||||||
@ -530,7 +536,7 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
|||||||
currency: Reference to the currency of this pricebreak (leave empty for base currency)
|
currency: Reference to the currency of this pricebreak (leave empty for base currency)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks')
|
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks', verbose_name=_('Part'),)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("part", "quantity")
|
unique_together = ("part", "quantity")
|
||||||
|
@ -43,17 +43,17 @@ InvenTree | {% trans "Company" %} - {{ company.name }}
|
|||||||
<p>{{ company.description }}</p>
|
<p>{{ company.description }}</p>
|
||||||
<div class='btn-group action-buttons'>
|
<div class='btn-group action-buttons'>
|
||||||
{% if company.is_supplier and roles.purchase_order.add %}
|
{% if company.is_supplier and roles.purchase_order.add %}
|
||||||
<button type='button' class='btn btn-default' id='company-order-2' title='Create purchase order'>
|
<button type='button' class='btn btn-default' id='company-order-2' title='{% trans "Create Purchase Order" %}'>
|
||||||
<span class='fas fa-shopping-cart'/>
|
<span class='fas fa-shopping-cart'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.company.change_company %}
|
{% if perms.company.change_company %}
|
||||||
<button type='button' class='btn btn-default' id='company-edit' title='Edit company information'>
|
<button type='button' class='btn btn-default' id='company-edit' title='{% trans "Edit company information" %}'>
|
||||||
<span class='fas fa-edit icon-green'/>
|
<span class='fas fa-edit icon-green'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.company.delete_company %}
|
{% if perms.company.delete_company %}
|
||||||
<button type='button' class='btn btn-default' id='company-delete' title='Delete company'>
|
<button type='button' class='btn btn-default' id='company-delete' title='{% trans "Delete Company" %}'>
|
||||||
<span class='fas fa-trash-alt icon-red'/>
|
<span class='fas fa-trash-alt icon-red'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
{% extends "modal_delete_form.html" %}
|
{% extends "modal_delete_form.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
Are you sure you want to delete company '{{ company.name }}'?
|
{% blocktrans with company.name as name %}Are you sure you want to delete company '{{ name }}'?{% endblocktrans %}
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
{% if company.supplied_part_count > 0 %}
|
{% if company.supplied_part_count > 0 %}
|
||||||
<p>There are {{ company.supplied_part_count }} parts sourced from this company.<br>
|
<p>{% blocktrans with company.supplied_part_count as count %}There are {{ count }} parts sourced from this company.<br>
|
||||||
If this supplier is deleted, these supplier part entries will also be deleted.</p>
|
If this supplier is deleted, these supplier part entries will also be deleted.{% endblocktrans %}</p>
|
||||||
<ul class='list-group'>
|
<ul class='list-group'>
|
||||||
{% for part in company.parts.all %}
|
{% for part in company.parts.all %}
|
||||||
<li class='list-group-item'><b>{{ part.SKU }}</b> - <i>{{ part.part.full_name }}</i></li>
|
<li class='list-group-item'><b>{{ part.SKU }}</b> - <i>{{ part.part.full_name }}</i></li>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
{% if roles.purchase_order.change %}
|
{% if roles.purchase_order.change %}
|
||||||
<div id='button-toolbar'>
|
<div id='button-toolbar'>
|
||||||
<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='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" %}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
{% if roles.purchase_order.add %}
|
{% if roles.purchase_order.add %}
|
||||||
<div id='button-bar'>
|
<div id='button-bar'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button class='btn btn-primary' type='button' id='order-part2' title='Order part'>
|
<button class='btn btn-primary' type='button' id='order-part2' title='{% trans "Order part" %}'>
|
||||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Part" %}</button>
|
<span class='fas fa-shopping-cart'></span> {% trans "Order Part" %}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
params: {
|
params: {
|
||||||
supplier_part: {{ part.id }},
|
supplier_part: {{ part.id }},
|
||||||
location_detail: true,
|
location_detail: true,
|
||||||
part_detail: true,
|
part_detail: false,
|
||||||
},
|
},
|
||||||
groupByField: 'location',
|
groupByField: 'location',
|
||||||
buttons: ['#stock-options'],
|
buttons: ['#stock-options'],
|
||||||
|
@ -6,7 +6,7 @@ Django views for interacting with Company app
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView, UpdateView
|
from django.views.generic import DetailView, ListView, UpdateView
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
@ -7,11 +7,9 @@
|
|||||||
# with the prefix INVENTREE_DB_
|
# with the prefix INVENTREE_DB_
|
||||||
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
|
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
|
||||||
database:
|
database:
|
||||||
# Default configuration - sqlite filesystem database
|
# Uncomment (and edit) one of the database configurations below,
|
||||||
ENGINE: sqlite3
|
# or specify database options using environment variables
|
||||||
NAME: '../inventree_default_db.sqlite3'
|
|
||||||
|
|
||||||
# For more complex database installations, further parameters are required
|
|
||||||
# Refer to the django documentation for full list of options
|
# Refer to the django documentation for full list of options
|
||||||
|
|
||||||
# --- Available options: ---
|
# --- Available options: ---
|
||||||
@ -27,14 +25,22 @@ database:
|
|||||||
|
|
||||||
# --- Example Configuration - sqlite3 ---
|
# --- Example Configuration - sqlite3 ---
|
||||||
# ENGINE: sqlite3
|
# ENGINE: sqlite3
|
||||||
# NAME: '/path/to/database.sqlite3'
|
# NAME: '/home/inventree/database.sqlite3'
|
||||||
|
|
||||||
# --- Example Configuration - MySQL ---
|
# --- Example Configuration - MySQL ---
|
||||||
#ENGINE: django.db.backends.mysql
|
#ENGINE: mysql
|
||||||
#NAME: inventree
|
#NAME: inventree
|
||||||
#USER: inventree_username
|
#USER: inventree
|
||||||
#PASSWORD: inventree_password
|
#PASSWORD: inventree_password
|
||||||
#HOST: '127.0.0.1'
|
#HOST: 'localhost'
|
||||||
|
#PORT: '3306'
|
||||||
|
|
||||||
|
# --- Example Configuration - Postgresql ---
|
||||||
|
#ENGINE: postgresql
|
||||||
|
#NAME: inventree
|
||||||
|
#USER: inventree
|
||||||
|
#PASSWORD: inventree_password
|
||||||
|
#HOST: 'localhost'
|
||||||
#PORT: '5432'
|
#PORT: '5432'
|
||||||
|
|
||||||
# Select default system language (default is 'en-us')
|
# Select default system language (default is 'en-us')
|
||||||
@ -43,6 +49,7 @@ language: en-us
|
|||||||
# System time-zone (default is UTC)
|
# System time-zone (default is UTC)
|
||||||
# Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
# Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
# Select an option from the "TZ database name" column
|
# Select an option from the "TZ database name" column
|
||||||
|
# Use the environment variable INVENTREE_TIMEZONE
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
|
|
||||||
# List of currencies supported by default.
|
# List of currencies supported by default.
|
||||||
@ -56,7 +63,33 @@ currencies:
|
|||||||
- NZD
|
- NZD
|
||||||
- USD
|
- USD
|
||||||
|
|
||||||
|
# Email backend configuration
|
||||||
|
# Ref: https://docs.djangoproject.com/en/dev/topics/email/
|
||||||
|
# Available options:
|
||||||
|
# host: Email server host address
|
||||||
|
# port: Email port
|
||||||
|
# username: Account username
|
||||||
|
# password: Account password
|
||||||
|
# prefix: Email subject prefix
|
||||||
|
# tls: Enable TLS support
|
||||||
|
# ssl: Enable SSL support
|
||||||
|
|
||||||
|
# Alternatively, these options can all be set using environment variables,
|
||||||
|
# with the INVENTREE_EMAIL_ prefix:
|
||||||
|
# e.g. INVENTREE_EMAIL_HOST / INVENTREE_EMAIL_PORT / INVENTREE_EMAIL_USERNAME
|
||||||
|
# Refer to the InvenTree documentation for more information
|
||||||
|
|
||||||
|
email:
|
||||||
|
# backend: 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
host: ''
|
||||||
|
port: 25
|
||||||
|
username: ''
|
||||||
|
password: ''
|
||||||
|
tls: False
|
||||||
|
ssl: False
|
||||||
|
|
||||||
# Set debug to False to run in production mode
|
# Set debug to False to run in production mode
|
||||||
|
# Use the environment variable INVENTREE_DEBUG
|
||||||
debug: True
|
debug: True
|
||||||
|
|
||||||
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
|
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
|
||||||
@ -65,6 +98,7 @@ debug: True
|
|||||||
debug_toolbar: False
|
debug_toolbar: False
|
||||||
|
|
||||||
# Configure the system logging level
|
# Configure the system logging level
|
||||||
|
# Use environment variable INVENTREE_LOG_LEVEL
|
||||||
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
|
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||||
log_level: WARNING
|
log_level: WARNING
|
||||||
|
|
||||||
@ -86,13 +120,14 @@ cors:
|
|||||||
# - https://sub.example.com
|
# - https://sub.example.com
|
||||||
|
|
||||||
# MEDIA_ROOT is the local filesystem location for storing uploaded files
|
# MEDIA_ROOT is the local filesystem location for storing uploaded files
|
||||||
# By default, it is stored in a directory named 'inventree_media' local to the InvenTree directory
|
# By default, it is stored under /home/inventree/data/media
|
||||||
# This should be changed for a production installation
|
# Use environment variable INVENTREE_MEDIA_ROOT
|
||||||
media_root: '../inventree_media'
|
media_root: '/home/inventree/data/media'
|
||||||
|
|
||||||
# STATIC_ROOT is the local filesystem location for storing static files
|
# STATIC_ROOT is the local filesystem location for storing static files
|
||||||
# By default it is stored in a directory named 'inventree_static' local to the InvenTree directory
|
# By default, it is stored under /home/inventree
|
||||||
static_root: '../inventree_static'
|
# Use environment variable INVENTREE_STATIC_ROOT
|
||||||
|
static_root: '/home/inventree/static'
|
||||||
|
|
||||||
# Optional URL schemes to allow in URL fields
|
# Optional URL schemes to allow in URL fields
|
||||||
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
|
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
|
||||||
@ -105,7 +140,8 @@ static_root: '../inventree_static'
|
|||||||
# Backup options
|
# Backup options
|
||||||
# Set the backup_dir parameter to store backup files in a specific location
|
# Set the backup_dir parameter to store backup files in a specific location
|
||||||
# If unspecified, the local user's temp directory will be used
|
# If unspecified, the local user's temp directory will be used
|
||||||
#backup_dir: '/home/inventree/backup/'
|
# Use environment variable INVENTREE_BACKUP_DIR
|
||||||
|
backup_dir: '/home/inventree/data/backup/'
|
||||||
|
|
||||||
# Permit custom authentication backends
|
# Permit custom authentication backends
|
||||||
#authentication_backends:
|
#authentication_backends:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.core.exceptions import ValidationError, FieldError
|
from django.core.exceptions import ValidationError, FieldError
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
@ -7,7 +7,7 @@ from django.apps import AppConfig
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
def hashFile(filename):
|
def hashFile(filename):
|
||||||
|
@ -32,7 +32,7 @@ except OSError as err:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
def rename_label(instance, filename):
|
def rename_label(instance, filename):
|
||||||
@ -126,7 +126,7 @@ class LabelTemplate(models.Model):
|
|||||||
|
|
||||||
width = models.FloatField(
|
width = models.FloatField(
|
||||||
default=50,
|
default=50,
|
||||||
verbose_name=('Width [mm]'),
|
verbose_name=_('Width [mm]'),
|
||||||
help_text=_('Label width, specified in mm'),
|
help_text=_('Label width, specified in mm'),
|
||||||
validators=[MinValueValidator(2)]
|
validators=[MinValueValidator(2)]
|
||||||
)
|
)
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ Django Forms for interacting with Order objects
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mptt.fields import TreeNodeChoiceField
|
from mptt.fields import TreeNodeChoiceField
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ from .models import SalesOrderAllocation
|
|||||||
|
|
||||||
class IssuePurchaseOrderForm(HelperForm):
|
class IssuePurchaseOrderForm(HelperForm):
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=True, initial=False, help_text=_('Place order'))
|
confirm = forms.BooleanField(required=True, initial=False, label=_('Confirm'), help_text=_('Place order'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
@ -35,7 +35,7 @@ class IssuePurchaseOrderForm(HelperForm):
|
|||||||
|
|
||||||
class CompletePurchaseOrderForm(HelperForm):
|
class CompletePurchaseOrderForm(HelperForm):
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=True, help_text=_("Mark order as complete"))
|
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_("Mark order as complete"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
@ -46,7 +46,7 @@ class CompletePurchaseOrderForm(HelperForm):
|
|||||||
|
|
||||||
class CancelPurchaseOrderForm(HelperForm):
|
class CancelPurchaseOrderForm(HelperForm):
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=True, help_text=_('Cancel order'))
|
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Cancel order'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
@ -57,7 +57,7 @@ class CancelPurchaseOrderForm(HelperForm):
|
|||||||
|
|
||||||
class CancelSalesOrderForm(HelperForm):
|
class CancelSalesOrderForm(HelperForm):
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=True, help_text=_('Cancel order'))
|
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Cancel order'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrder
|
model = SalesOrder
|
||||||
@ -68,7 +68,7 @@ class CancelSalesOrderForm(HelperForm):
|
|||||||
|
|
||||||
class ShipSalesOrderForm(HelperForm):
|
class ShipSalesOrderForm(HelperForm):
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=True, help_text=_('Ship order'))
|
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Ship order'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrder
|
model = SalesOrder
|
||||||
@ -79,7 +79,7 @@ class ShipSalesOrderForm(HelperForm):
|
|||||||
|
|
||||||
class ReceivePurchaseOrderForm(HelperForm):
|
class ReceivePurchaseOrderForm(HelperForm):
|
||||||
|
|
||||||
location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), required=True, help_text=_('Receive parts to this location'))
|
location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), required=True, label=_('Location'), help_text=_('Receive parts to this location'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
@ -106,6 +106,7 @@ class EditPurchaseOrderForm(HelperForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
target_date = DatePickerFormField(
|
target_date = DatePickerFormField(
|
||||||
|
label=_('Target Date'),
|
||||||
help_text=_('Target date for order delivery. Order will be overdue after this date.'),
|
help_text=_('Target date for order delivery. Order will be overdue after this date.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -140,6 +141,7 @@ class EditSalesOrderForm(HelperForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
target_date = DatePickerFormField(
|
target_date = DatePickerFormField(
|
||||||
|
label=_('Target Date'),
|
||||||
help_text=_('Target date for order completion. Order will be overdue after this date.'),
|
help_text=_('Target date for order completion. Order will be overdue after this date.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -183,7 +185,7 @@ class EditSalesOrderAttachmentForm(HelperForm):
|
|||||||
class EditPurchaseOrderLineItemForm(HelperForm):
|
class EditPurchaseOrderLineItemForm(HelperForm):
|
||||||
""" Form for editing a PurchaseOrderLineItem object """
|
""" Form for editing a PurchaseOrderLineItem object """
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrderLineItem
|
model = PurchaseOrderLineItem
|
||||||
@ -200,7 +202,7 @@ class EditPurchaseOrderLineItemForm(HelperForm):
|
|||||||
class EditSalesOrderLineItemForm(HelperForm):
|
class EditSalesOrderLineItemForm(HelperForm):
|
||||||
""" Form for editing a SalesOrderLineItem object """
|
""" Form for editing a SalesOrderLineItem object """
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrderLineItem
|
model = SalesOrderLineItem
|
||||||
@ -256,7 +258,7 @@ class CreateSalesOrderAllocationForm(HelperForm):
|
|||||||
Form for creating a SalesOrderAllocation item.
|
Form for creating a SalesOrderAllocation item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrderAllocation
|
model = SalesOrderAllocation
|
||||||
@ -273,7 +275,7 @@ class EditSalesOrderAllocationForm(HelperForm):
|
|||||||
Form for editing a SalesOrderAllocation item
|
Form for editing a SalesOrderAllocation item
|
||||||
"""
|
"""
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrderAllocation
|
model = SalesOrderAllocation
|
||||||
|
233
InvenTree/order/migrations/0044_auto_20210404_2016.py
Normal file
233
InvenTree/order/migrations/0044_auto_20210404_2016.py
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-04-04 20:16
|
||||||
|
|
||||||
|
import InvenTree.fields
|
||||||
|
import InvenTree.models
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import markdownx.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('company', '0032_auto_20210403_1837'),
|
||||||
|
('part', '0063_bomitem_inherited'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('stock', '0058_stockitem_packaging'),
|
||||||
|
('order', '0043_auto_20210330_0013'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='creation_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Creation Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(help_text='Order description', max_length=250, verbose_name='Description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='link',
|
||||||
|
field=models.URLField(blank=True, help_text='Link to external page', verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='notes',
|
||||||
|
field=markdownx.models.MarkdownxField(blank=True, help_text='Order notes', verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='received_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='received by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='reference',
|
||||||
|
field=models.CharField(help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='supplier',
|
||||||
|
field=models.ForeignKey(help_text='Company from which the items are being ordered', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='purchase_orders', to='company.Company', verbose_name='Supplier'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='supplier_reference',
|
||||||
|
field=models.CharField(blank=True, help_text='Supplier order reference code', max_length=64, verbose_name='Supplier Reference'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderattachment',
|
||||||
|
name='comment',
|
||||||
|
field=models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderattachment',
|
||||||
|
name='upload_date',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True, verbose_name='upload date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderattachment',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='notes',
|
||||||
|
field=models.CharField(blank=True, help_text='Line item notes', max_length=500, verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='order',
|
||||||
|
field=models.ForeignKey(help_text='Purchase Order', on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='order.PurchaseOrder', verbose_name='Order'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_order_line_items', to='company.SupplierPart', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='quantity',
|
||||||
|
field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='received',
|
||||||
|
field=models.DecimalField(decimal_places=5, default=0, help_text='Number of items received', max_digits=15, verbose_name='Received'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='reference',
|
||||||
|
field=models.CharField(blank=True, help_text='Line item reference', max_length=100, verbose_name='Reference'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='creation_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Creation Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='customer',
|
||||||
|
field=models.ForeignKey(help_text='Company to which the items are being sold', limit_choices_to={'is_customer': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_orders', to='company.Company', verbose_name='Customer'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='customer_reference',
|
||||||
|
field=models.CharField(blank=True, help_text='Customer order reference code', max_length=64, verbose_name='Customer Reference '),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(help_text='Order description', max_length=250, verbose_name='Description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='link',
|
||||||
|
field=models.URLField(blank=True, help_text='Link to external page', verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='notes',
|
||||||
|
field=markdownx.models.MarkdownxField(blank=True, help_text='Order notes', verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='reference',
|
||||||
|
field=models.CharField(help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='shipment_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Shipment Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='shipped_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='shipped by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='status',
|
||||||
|
field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Shipped'), (40, 'Cancelled'), (50, 'Lost'), (60, 'Returned')], default=10, help_text='Purchase order status', verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderallocation',
|
||||||
|
name='item',
|
||||||
|
field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'belongs_to': None, 'part__salable': True, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem', verbose_name='Item'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderallocation',
|
||||||
|
name='line',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='order.SalesOrderLineItem', verbose_name='Line'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderallocation',
|
||||||
|
name='quantity',
|
||||||
|
field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Enter stock allocation quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderattachment',
|
||||||
|
name='comment',
|
||||||
|
field=models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderattachment',
|
||||||
|
name='upload_date',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True, verbose_name='upload date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderattachment',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderlineitem',
|
||||||
|
name='notes',
|
||||||
|
field=models.CharField(blank=True, help_text='Line item notes', max_length=500, verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderlineitem',
|
||||||
|
name='order',
|
||||||
|
field=models.ForeignKey(help_text='Sales Order', on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='order.SalesOrder', verbose_name='Order'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderlineitem',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(help_text='Part', limit_choices_to={'salable': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_order_line_items', to='part.Part', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderlineitem',
|
||||||
|
name='quantity',
|
||||||
|
field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderlineitem',
|
||||||
|
name='reference',
|
||||||
|
field=models.CharField(blank=True, help_text='Line item reference', max_length=100, verbose_name='Reference'),
|
||||||
|
),
|
||||||
|
]
|
@ -15,7 +15,7 @@ from django.core.validators import MinValueValidator
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
|
|
||||||
@ -96,18 +96,19 @@ class Order(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
reference = models.CharField(unique=True, max_length=64, blank=False, help_text=_('Order reference'))
|
reference = models.CharField(unique=True, max_length=64, blank=False, verbose_name=_('Reference'), help_text=_('Order reference'))
|
||||||
|
|
||||||
description = models.CharField(max_length=250, help_text=_('Order description'))
|
description = models.CharField(max_length=250, verbose_name=_('Description'), help_text=_('Order description'))
|
||||||
|
|
||||||
link = models.URLField(blank=True, help_text=_('Link to external page'))
|
link = models.URLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page'))
|
||||||
|
|
||||||
creation_date = models.DateField(blank=True, null=True)
|
creation_date = models.DateField(blank=True, null=True, verbose_name=_('Creation Date'))
|
||||||
|
|
||||||
created_by = models.ForeignKey(User,
|
created_by = models.ForeignKey(User,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
related_name='+'
|
related_name='+',
|
||||||
|
verbose_name=_('Created By')
|
||||||
)
|
)
|
||||||
|
|
||||||
responsible = models.ForeignKey(
|
responsible = models.ForeignKey(
|
||||||
@ -119,7 +120,7 @@ class Order(models.Model):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
)
|
)
|
||||||
|
|
||||||
notes = MarkdownxField(blank=True, help_text=_('Order notes'))
|
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrder(Order):
|
class PurchaseOrder(Order):
|
||||||
@ -186,16 +187,18 @@ class PurchaseOrder(Order):
|
|||||||
'is_supplier': True,
|
'is_supplier': True,
|
||||||
},
|
},
|
||||||
related_name='purchase_orders',
|
related_name='purchase_orders',
|
||||||
|
verbose_name=_('Supplier'),
|
||||||
help_text=_('Company from which the items are being ordered')
|
help_text=_('Company from which the items are being ordered')
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_reference = models.CharField(max_length=64, blank=True, help_text=_("Supplier order reference code"))
|
supplier_reference = models.CharField(max_length=64, blank=True, verbose_name=_('Supplier Reference'), help_text=_("Supplier order reference code"))
|
||||||
|
|
||||||
received_by = models.ForeignKey(
|
received_by = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
related_name='+'
|
related_name='+',
|
||||||
|
verbose_name=_('received by')
|
||||||
)
|
)
|
||||||
|
|
||||||
issue_date = models.DateField(
|
issue_date = models.DateField(
|
||||||
@ -435,13 +438,14 @@ class SalesOrder(Order):
|
|||||||
null=True,
|
null=True,
|
||||||
limit_choices_to={'is_customer': True},
|
limit_choices_to={'is_customer': True},
|
||||||
related_name='sales_orders',
|
related_name='sales_orders',
|
||||||
|
verbose_name=_('Customer'),
|
||||||
help_text=_("Company to which the items are being sold"),
|
help_text=_("Company to which the items are being sold"),
|
||||||
)
|
)
|
||||||
|
|
||||||
status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(),
|
status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(),
|
||||||
help_text=_('Purchase order status'))
|
verbose_name=_('Status'), help_text=_('Purchase order status'))
|
||||||
|
|
||||||
customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code"))
|
customer_reference = models.CharField(max_length=64, blank=True, verbose_name=_('Customer Reference '), help_text=_("Customer order reference code"))
|
||||||
|
|
||||||
target_date = models.DateField(
|
target_date = models.DateField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@ -449,13 +453,14 @@ class SalesOrder(Order):
|
|||||||
help_text=_('Target date for order completion. Order will be overdue after this date.')
|
help_text=_('Target date for order completion. Order will be overdue after this date.')
|
||||||
)
|
)
|
||||||
|
|
||||||
shipment_date = models.DateField(blank=True, null=True)
|
shipment_date = models.DateField(blank=True, null=True, verbose_name=_('Shipment Date'))
|
||||||
|
|
||||||
shipped_by = models.ForeignKey(
|
shipped_by = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
related_name='+'
|
related_name='+',
|
||||||
|
verbose_name=_('shipped by')
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -586,11 +591,11 @@ class OrderLineItem(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, help_text=_('Item quantity'))
|
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'), help_text=_('Item quantity'))
|
||||||
|
|
||||||
reference = models.CharField(max_length=100, blank=True, help_text=_('Line item reference'))
|
reference = models.CharField(max_length=100, blank=True, verbose_name=_('Reference'), help_text=_('Line item reference'))
|
||||||
|
|
||||||
notes = models.CharField(max_length=500, blank=True, help_text=_('Line item notes'))
|
notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes'))
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItem(OrderLineItem):
|
class PurchaseOrderLineItem(OrderLineItem):
|
||||||
@ -616,6 +621,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
PurchaseOrder, on_delete=models.CASCADE,
|
PurchaseOrder, on_delete=models.CASCADE,
|
||||||
related_name='lines',
|
related_name='lines',
|
||||||
|
verbose_name=_('Order'),
|
||||||
help_text=_('Purchase Order')
|
help_text=_('Purchase Order')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -629,10 +635,11 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
SupplierPart, on_delete=models.SET_NULL,
|
SupplierPart, on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
related_name='purchase_order_line_items',
|
related_name='purchase_order_line_items',
|
||||||
|
verbose_name=_('Part'),
|
||||||
help_text=_("Supplier part"),
|
help_text=_("Supplier part"),
|
||||||
)
|
)
|
||||||
|
|
||||||
received = models.DecimalField(decimal_places=5, max_digits=15, default=0, help_text=_('Number of items received'))
|
received = models.DecimalField(decimal_places=5, max_digits=15, default=0, verbose_name=_('Received'), help_text=_('Number of items received'))
|
||||||
|
|
||||||
purchase_price = MoneyField(
|
purchase_price = MoneyField(
|
||||||
max_digits=19,
|
max_digits=19,
|
||||||
@ -658,9 +665,9 @@ class SalesOrderLineItem(OrderLineItem):
|
|||||||
part: Link to a Part object (may be null)
|
part: Link to a Part object (may be null)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', help_text=_('Sales Order'))
|
order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', verbose_name=_('Order'), help_text=_('Sales Order'))
|
||||||
|
|
||||||
part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, help_text=_('Part'), limit_choices_to={'salable': True})
|
part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [
|
unique_together = [
|
||||||
@ -760,7 +767,7 @@ class SalesOrderAllocation(models.Model):
|
|||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, related_name='allocations')
|
line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, verbose_name=_('Line'), related_name='allocations')
|
||||||
|
|
||||||
item = models.ForeignKey(
|
item = models.ForeignKey(
|
||||||
'stock.StockItem',
|
'stock.StockItem',
|
||||||
@ -771,10 +778,11 @@ class SalesOrderAllocation(models.Model):
|
|||||||
'belongs_to': None,
|
'belongs_to': None,
|
||||||
'sales_order': None,
|
'sales_order': None,
|
||||||
},
|
},
|
||||||
|
verbose_name=_('Item'),
|
||||||
help_text=_('Select stock item to allocate')
|
help_text=_('Select stock item to allocate')
|
||||||
)
|
)
|
||||||
|
|
||||||
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, help_text=_('Enter stock allocation quantity'))
|
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'), help_text=_('Enter stock allocation quantity'))
|
||||||
|
|
||||||
def get_serial(self):
|
def get_serial(self):
|
||||||
return self.item.serial
|
return self.item.serial
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
{% extends "modal_form.html" %}
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
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'>
|
||||||
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.
|
Marking this order as complete will remove these line items.' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
{% extends "modal_form.html" %}
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
After placing this purchase order, line items will no longer be editable.
|
{% trans 'After placing this purchase order, line items will no longer be editable.' %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -39,7 +39,7 @@
|
|||||||
{{ part.full_name }} <small><i>{{ part.description }}</i></small>
|
{{ part.full_name }} <small><i>{{ part.description }}</i></small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class='btn btn-default btn-create' onClick='newSupplierPartFromOrderWizard()' id='new_supplier_part_{{ part.id }}' part='{{ part.pk }}' title='{% trans "Create new supplier part" $}' type='button'>
|
<button class='btn btn-default btn-create' onClick='newSupplierPartFromOrderWizard()' id='new_supplier_part_{{ part.id }}' part='{{ part.pk }}' title='{% trans "Create new supplier part" %}' type='button'>
|
||||||
<span part='{{ part.pk }}' class='fas fa-plus-circle'></span>
|
<span part='{{ part.pk }}' class='fas fa-plus-circle'></span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -66,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class='btn btn-default btn-remove' onclick='removeOrderRowFromOrderWizard()' id='del_item_{{ part.id }}' title='Remove part' type='button'>
|
<button class='btn btn-default btn-remove' onclick='removeOrderRowFromOrderWizard()' id='del_item_{{ part.id }}' title='{% trans "Remove part" %}' type='button'>
|
||||||
<span row='part_row_{{ part.id }}' class='fas fa-trash-alt icon-red'></span>
|
<span row='part_row_{{ part.id }}' class='fas fa-trash-alt icon-red'></span>
|
||||||
</button>
|
</button>
|
||||||
</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='Create new purchase order for {{ supplier.name }}'
|
title='{% trans "Create new purchase order for {{ supplier.name }}" %}'
|
||||||
type='button'
|
type='button'
|
||||||
supplierid='{{ supplier.id }}'
|
supplierid='{{ supplier.id }}'
|
||||||
onclick='newPurchaseOrderFromOrderWizard()'>
|
onclick='newPurchaseOrderFromOrderWizard()'>
|
||||||
|
@ -116,6 +116,7 @@ InvenTree | {% trans "Purchase Orders" %}
|
|||||||
initialView: 'dayGridMonth',
|
initialView: 'dayGridMonth',
|
||||||
nowIndicator: true,
|
nowIndicator: true,
|
||||||
aspectRatio: 2.5,
|
aspectRatio: 2.5,
|
||||||
|
locale: '{{request.LANGUAGE_CODE}}',
|
||||||
datesSet: function() {
|
datesSet: function() {
|
||||||
loadOrderEvents(calendar);
|
loadOrderEvents(calendar);
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='Remove line' type='button'>
|
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
||||||
<span row='line_row_{{ line.id }}' class='fas fa-times-circle icon-red'></span>
|
<span row='line_row_{{ line.id }}' class='fas fa-times-circle icon-red'></span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -49,7 +49,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<span class='fas fa-print'></span>
|
<span class='fas fa-print'></span>
|
||||||
</button>
|
</button>
|
||||||
{% if roles.sales_order.change %}
|
{% if roles.sales_order.change %}
|
||||||
<button type='button' class='btn btn-default' id='edit-order' title='Edit order information'>
|
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
<span class='fas fa-edit icon-green'></span>
|
||||||
</button>
|
</button>
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
|
@ -67,7 +67,7 @@ function showAllocationSubTable(index, row, element) {
|
|||||||
{
|
{
|
||||||
width: '50%',
|
width: '50%',
|
||||||
field: 'allocated',
|
field: 'allocated',
|
||||||
title: 'Quantity',
|
title: '{% trans "Quantity" %}',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
var text = '';
|
var text = '';
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ function showAllocationSubTable(index, row, element) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'buttons',
|
field: 'buttons',
|
||||||
title: 'Actions',
|
title: '{% trans "Actions" %}',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
var html = "<div class='btn-group float-right' role='group'>";
|
var html = "<div class='btn-group float-right' role='group'>";
|
||||||
@ -167,7 +167,7 @@ function showFulfilledSubTable(index, row, element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$("#so-lines-table").inventreeTable({
|
$("#so-lines-table").inventreeTable({
|
||||||
formatNoMatches: function() { return "No matching line items"; },
|
formatNoMatches: function() { return "{% trans 'No matching line items' %}"; },
|
||||||
queryParams: {
|
queryParams: {
|
||||||
order: {{ order.id }},
|
order: {{ order.id }},
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
@ -196,7 +196,7 @@ $("#so-lines-table").inventreeTable({
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
field: 'pk',
|
||||||
title: 'ID',
|
title: '{% trans "ID" %}',
|
||||||
visible: false,
|
visible: false,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
@ -204,7 +204,7 @@ $("#so-lines-table").inventreeTable({
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
sortName: 'part__name',
|
sortName: 'part__name',
|
||||||
field: 'part',
|
field: 'part',
|
||||||
title: 'Part',
|
title: '{% trans "Part" %}',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
if (row.part) {
|
if (row.part) {
|
||||||
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`);
|
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`);
|
||||||
@ -216,12 +216,12 @@ $("#so-lines-table").inventreeTable({
|
|||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'reference',
|
field: 'reference',
|
||||||
title: 'Reference'
|
title: '{% trans "Reference" %}'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
title: 'Quantity',
|
title: '{% trans "Quantity" %}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'allocated',
|
field: 'allocated',
|
||||||
@ -261,7 +261,7 @@ $("#so-lines-table").inventreeTable({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'notes',
|
field: 'notes',
|
||||||
title: 'Notes',
|
title: '{% trans "Notes" %}',
|
||||||
},
|
},
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
{
|
{
|
||||||
|
@ -115,6 +115,7 @@ InvenTree | {% trans "Sales Orders" %}
|
|||||||
initialView: 'dayGridMonth',
|
initialView: 'dayGridMonth',
|
||||||
nowIndicator: true,
|
nowIndicator: true,
|
||||||
aspectRatio: 2.5,
|
aspectRatio: 2.5,
|
||||||
|
locale: '{{request.LANGUAGE_CODE}}',
|
||||||
datesSet: function() {
|
datesSet: function() {
|
||||||
loadOrderEvents(calendar);
|
loadOrderEvents(calendar);
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ from django.db import transaction
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView, UpdateView
|
from django.views.generic import DetailView, ListView, UpdateView
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django.forms import HiddenInput
|
from django.forms import HiddenInput
|
||||||
@ -37,7 +37,7 @@ from InvenTree.views import InvenTreeRoleMixin
|
|||||||
|
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
||||||
@ -1058,7 +1058,7 @@ class OrderParts(AjaxView):
|
|||||||
|
|
||||||
data = {
|
data = {
|
||||||
'form_valid': valid,
|
'form_valid': valid,
|
||||||
'success': 'Ordered {n} parts'.format(n=len(self.parts))
|
'success': _('Ordered {n} parts').format(n=len(self.parts))
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.renderJsonResponse(self.request, data=data)
|
return self.renderJsonResponse(self.request, data=data)
|
||||||
@ -1349,7 +1349,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin):
|
|||||||
'form_valid': valid,
|
'form_valid': valid,
|
||||||
'form_errors': self.form.errors.as_json(),
|
'form_errors': self.form.errors.as_json(),
|
||||||
'non_field_errors': self.form.non_field_errors().as_json(),
|
'non_field_errors': self.form.non_field_errors().as_json(),
|
||||||
'success': _("Allocated") + f" {len(self.stock_items)} " + _("items")
|
'success': _("Allocated {n} items").format(n=len(self.stock_items))
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.renderJsonResponse(request, self.form, data)
|
return self.renderJsonResponse(request, self.form, data)
|
||||||
|
@ -16,8 +16,6 @@ from .models import PartCategoryParameterTemplate
|
|||||||
from .models import PartTestTemplate
|
from .models import PartTestTemplate
|
||||||
from .models import PartSellPriceBreak
|
from .models import PartSellPriceBreak
|
||||||
|
|
||||||
from InvenTree.helpers import normalize
|
|
||||||
|
|
||||||
from stock.models import StockLocation
|
from stock.models import StockLocation
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
|
|
||||||
@ -180,7 +178,7 @@ class BomItemResource(ModelResource):
|
|||||||
|
|
||||||
Ref: https://django-import-export.readthedocs.io/en/latest/getting_started.html#advanced-data-manipulation-on-export
|
Ref: https://django-import-export.readthedocs.io/en/latest/getting_started.html#advanced-data-manipulation-on-export
|
||||||
"""
|
"""
|
||||||
return normalize(item.quantity)
|
return float(item.quantity)
|
||||||
|
|
||||||
def before_export(self, queryset, *args, **kwargs):
|
def before_export(self, queryset, *args, **kwargs):
|
||||||
|
|
||||||
|
@ -735,6 +735,15 @@ class PartParameterList(generics.ListCreateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for detail view of a single PartParameter object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = PartParameter.objects.all()
|
||||||
|
serializer_class = part_serializers.PartParameterSerializer
|
||||||
|
|
||||||
|
|
||||||
class BomList(generics.ListCreateAPIView):
|
class BomList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of BomItem objects.
|
""" API endpoint for accessing a list of BomItem objects.
|
||||||
|
|
||||||
@ -942,6 +951,8 @@ part_api_urls = [
|
|||||||
# Base URL for PartParameter API endpoints
|
# Base URL for PartParameter API endpoints
|
||||||
url(r'^parameter/', include([
|
url(r'^parameter/', include([
|
||||||
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),
|
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-param-detail'),
|
||||||
url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
|
url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
from PIL import UnidentifiedImageError
|
from PIL import UnidentifiedImageError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class PartConfig(AppConfig):
|
class PartConfig(AppConfig):
|
||||||
|
@ -275,7 +275,7 @@ class BomUploadManager:
|
|||||||
elif ext in ['.xls', '.xlsx']:
|
elif ext in ['.xls', '.xlsx']:
|
||||||
raw_data = bom_file.read()
|
raw_data = bom_file.read()
|
||||||
else:
|
else:
|
||||||
raise ValidationError({'bom_file': _('Unsupported file format: {f}'.format(f=ext))})
|
raise ValidationError({'bom_file': _('Unsupported file format: {f}').format(f=ext)})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.data = tablib.Dataset().load(raw_data)
|
self.data = tablib.Dataset().load(raw_data)
|
||||||
|
@ -33,6 +33,27 @@
|
|||||||
template: 1
|
template: 1
|
||||||
data: 12
|
data: 12
|
||||||
|
|
||||||
|
- model: part.PartParameter
|
||||||
|
pk: 3
|
||||||
|
fields:
|
||||||
|
part: 3
|
||||||
|
template: 1
|
||||||
|
data: 12
|
||||||
|
|
||||||
|
- model: part.PartParameter
|
||||||
|
pk: 4
|
||||||
|
fields:
|
||||||
|
part: 3
|
||||||
|
template: 2
|
||||||
|
data: 12
|
||||||
|
|
||||||
|
- model: part.PartParameter
|
||||||
|
pk: 5
|
||||||
|
fields:
|
||||||
|
part: 3
|
||||||
|
template: 3
|
||||||
|
data: 12
|
||||||
|
|
||||||
# Add some template parameters to categories (requires category.yaml)
|
# Add some template parameters to categories (requires category.yaml)
|
||||||
- model: part.PartCategoryParameterTemplate
|
- model: part.PartCategoryParameterTemplate
|
||||||
pk: 1
|
pk: 1
|
||||||
|
@ -11,7 +11,7 @@ from InvenTree.fields import RoundingDecimalFormField
|
|||||||
|
|
||||||
from mptt.fields import TreeNodeChoiceField
|
from mptt.fields import TreeNodeChoiceField
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
@ -129,6 +129,7 @@ class BomDuplicateForm(HelperForm):
|
|||||||
|
|
||||||
confirm = forms.BooleanField(
|
confirm = forms.BooleanField(
|
||||||
required=False, initial=False,
|
required=False, initial=False,
|
||||||
|
label=_('Confirm'),
|
||||||
help_text=_('Confirm BOM duplication')
|
help_text=_('Confirm BOM duplication')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -147,7 +148,7 @@ class BomValidateForm(HelperForm):
|
|||||||
to confirm that the BOM for this part is valid
|
to confirm that the BOM for this part is valid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
validate = forms.BooleanField(required=False, initial=False, help_text=_('Confirm that the BOM is correct'))
|
validate = forms.BooleanField(required=False, initial=False, label=_('validate'), help_text=_('Confirm that the BOM is correct'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Part
|
model = Part
|
||||||
@ -159,7 +160,7 @@ class BomValidateForm(HelperForm):
|
|||||||
class BomUploadSelectFile(HelperForm):
|
class BomUploadSelectFile(HelperForm):
|
||||||
""" Form for importing a BOM. Provides a file input box for upload """
|
""" Form for importing a BOM. Provides a file input box for upload """
|
||||||
|
|
||||||
bom_file = forms.FileField(label='BOM file', required=True, help_text=_("Select BOM file to upload"))
|
bom_file = forms.FileField(label=_('BOM file'), required=True, help_text=_("Select BOM file to upload"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Part
|
model = Part
|
||||||
@ -336,9 +337,9 @@ class EditCategoryParameterTemplateForm(HelperForm):
|
|||||||
class EditBomItemForm(HelperForm):
|
class EditBomItemForm(HelperForm):
|
||||||
""" Form for editing a BomItem object """
|
""" Form for editing a BomItem object """
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||||
|
|
||||||
sub_part = PartModelChoiceField(queryset=Part.objects.all())
|
sub_part = PartModelChoiceField(queryset=Part.objects.all(), label=_('Sub part'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BomItem
|
model = BomItem
|
||||||
@ -365,6 +366,7 @@ class PartPriceForm(forms.Form):
|
|||||||
quantity = forms.IntegerField(
|
quantity = forms.IntegerField(
|
||||||
required=True,
|
required=True,
|
||||||
initial=1,
|
initial=1,
|
||||||
|
label=_('Quantity'),
|
||||||
help_text=_('Input quantity for price calculation')
|
help_text=_('Input quantity for price calculation')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -380,7 +382,7 @@ class EditPartSalePriceBreakForm(HelperForm):
|
|||||||
Form for creating / editing a sale price for a part
|
Form for creating / editing a sale price for a part
|
||||||
"""
|
"""
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PartSellPriceBreak
|
model = PartSellPriceBreak
|
||||||
|
218
InvenTree/part/migrations/0064_auto_20210404_2016.py
Normal file
218
InvenTree/part/migrations/0064_auto_20210404_2016.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-04-04 20:16
|
||||||
|
|
||||||
|
import InvenTree.models
|
||||||
|
import InvenTree.validators
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
import part.models
|
||||||
|
import stdimage.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('stock', '0058_stockitem_packaging'),
|
||||||
|
('part', '0063_bomitem_inherited'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='checksum',
|
||||||
|
field=models.CharField(blank=True, help_text='BOM line checksum', max_length=128, verbose_name='Checksum'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='note',
|
||||||
|
field=models.CharField(blank=True, help_text='BOM item notes', max_length=500, verbose_name='Note'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='optional',
|
||||||
|
field=models.BooleanField(default=False, help_text='This BOM item is optional', verbose_name='Optional'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='overage',
|
||||||
|
field=models.CharField(blank=True, help_text='Estimated build wastage quantity (absolute or percentage)', max_length=24, validators=[InvenTree.validators.validate_overage], verbose_name='Overage'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(help_text='Select parent part', limit_choices_to={'assembly': True}, on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=5, default=1.0, help_text='BOM quantity for this BOM item', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='reference',
|
||||||
|
field=models.CharField(blank=True, help_text='BOM item reference', max_length=500, verbose_name='Reference'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='sub_part',
|
||||||
|
field=models.ForeignKey(help_text='Select part to be used in BOM', limit_choices_to={'component': True}, on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part', verbose_name='Sub part'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='part',
|
||||||
|
name='bom_checked_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='boms_checked', to=settings.AUTH_USER_MODEL, verbose_name='BOM checked by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='part',
|
||||||
|
name='bom_checked_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='BOM checked date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='part',
|
||||||
|
name='bom_checksum',
|
||||||
|
field=models.CharField(blank=True, help_text='Stored BOM checksum', max_length=128, verbose_name='BOM checksum'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='part',
|
||||||
|
name='creation_date',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True, verbose_name='Creation Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='part',
|
||||||
|
name='creation_user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parts_created', to=settings.AUTH_USER_MODEL, verbose_name='Creation User'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='part',
|
||||||
|
name='image',
|
||||||
|
field=stdimage.models.StdImageField(blank=True, null=True, upload_to=part.models.rename_part_image, verbose_name='Image'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='part',
|
||||||
|
name='responsible',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parts_responible', to=settings.AUTH_USER_MODEL, verbose_name='Responsible'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partattachment',
|
||||||
|
name='comment',
|
||||||
|
field=models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partattachment',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='part.Part', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partattachment',
|
||||||
|
name='upload_date',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True, verbose_name='upload date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partattachment',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partcategory',
|
||||||
|
name='default_keywords',
|
||||||
|
field=models.CharField(blank=True, help_text='Default keywords for parts in this category', max_length=250, null=True, verbose_name='Default keywords'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partcategory',
|
||||||
|
name='default_location',
|
||||||
|
field=mptt.fields.TreeForeignKey(blank=True, help_text='Default location for parts in this category', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_categories', to='stock.StockLocation', verbose_name='Default Location'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partcategory',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, help_text='Description (optional)', max_length=250, verbose_name='Description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partcategory',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Name', max_length=100, validators=[InvenTree.validators.validate_tree_name], verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partcategory',
|
||||||
|
name='parent',
|
||||||
|
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='part.PartCategory', verbose_name='parent'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partcategoryparametertemplate',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(help_text='Part Category', on_delete=django.db.models.deletion.CASCADE, related_name='parameter_templates', to='part.PartCategory', verbose_name='Category'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partcategoryparametertemplate',
|
||||||
|
name='default_value',
|
||||||
|
field=models.CharField(blank=True, help_text='Default Parameter Value', max_length=500, verbose_name='Default Value'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partcategoryparametertemplate',
|
||||||
|
name='parameter_template',
|
||||||
|
field=models.ForeignKey(help_text='Parameter Template', on_delete=django.db.models.deletion.CASCADE, related_name='part_categories', to='part.PartParameterTemplate', verbose_name='Parameter Template'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partparameter',
|
||||||
|
name='data',
|
||||||
|
field=models.CharField(help_text='Parameter Value', max_length=500, verbose_name='Data'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partparameter',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(help_text='Parent Part', on_delete=django.db.models.deletion.CASCADE, related_name='parameters', to='part.Part', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partparameter',
|
||||||
|
name='template',
|
||||||
|
field=models.ForeignKey(help_text='Parameter Template', on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='part.PartParameterTemplate', verbose_name='Template'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partparametertemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Parameter Name', max_length=100, unique=True, verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partparametertemplate',
|
||||||
|
name='units',
|
||||||
|
field=models.CharField(blank=True, help_text='Parameter Units', max_length=25, verbose_name='Units'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partrelated',
|
||||||
|
name='part_1',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_1', to='part.Part', verbose_name='Part 1'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partrelated',
|
||||||
|
name='part_2',
|
||||||
|
field=models.ForeignKey(help_text='Select Related Part', on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_2', to='part.Part', verbose_name='Part 2'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partsellpricebreak',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(limit_choices_to={'salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='salepricebreaks', to='part.Part', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partstar',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_users', to='part.Part', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partstar',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_parts', to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parttesttemplate',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(limit_choices_to={'trackable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='test_templates', to='part.Part', verbose_name='Part'),
|
||||||
|
),
|
||||||
|
]
|
@ -52,7 +52,7 @@ import common.models
|
|||||||
import part.settings as part_settings
|
import part.settings as part_settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class PartCategory(InvenTreeTree):
|
class PartCategory(InvenTreeTree):
|
||||||
@ -69,10 +69,11 @@ class PartCategory(InvenTreeTree):
|
|||||||
'stock.StockLocation', related_name="default_categories",
|
'stock.StockLocation', related_name="default_categories",
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_('Default Location'),
|
||||||
help_text=_('Default location for parts in this category')
|
help_text=_('Default location for parts in this category')
|
||||||
)
|
)
|
||||||
|
|
||||||
default_keywords = models.CharField(null=True, blank=True, max_length=250, help_text=_('Default keywords for parts in this category'))
|
default_keywords = models.CharField(null=True, blank=True, max_length=250, verbose_name=_('Default keywords'), help_text=_('Default keywords for parts in this category'))
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('category-detail', kwargs={'pk': self.id})
|
return reverse('category-detail', kwargs={'pk': self.id})
|
||||||
@ -442,10 +443,10 @@ class Part(MPTTModel):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.pk == parent.pk:
|
if self.pk == parent.pk:
|
||||||
raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)".format(
|
raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format(
|
||||||
p1=str(self),
|
p1=str(self),
|
||||||
p2=str(parent)
|
p2=str(parent)
|
||||||
))})
|
)})
|
||||||
|
|
||||||
bom_items = self.get_bom_items()
|
bom_items = self.get_bom_items()
|
||||||
|
|
||||||
@ -454,10 +455,10 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
# Check for simple match
|
# Check for simple match
|
||||||
if item.sub_part == parent:
|
if item.sub_part == parent:
|
||||||
raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)".format(
|
raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format(
|
||||||
p1=str(parent),
|
p1=str(parent),
|
||||||
p2=str(self)
|
p2=str(self)
|
||||||
))})
|
)})
|
||||||
|
|
||||||
# And recursively check too
|
# And recursively check too
|
||||||
item.sub_part.checkAddToBOM(parent)
|
item.sub_part.checkAddToBOM(parent)
|
||||||
@ -749,6 +750,7 @@ class Part(MPTTModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
variations={'thumbnail': (128, 128)},
|
variations={'thumbnail': (128, 128)},
|
||||||
delete_orphans=False,
|
delete_orphans=False,
|
||||||
|
verbose_name=_('Image'),
|
||||||
)
|
)
|
||||||
|
|
||||||
default_location = TreeForeignKey(
|
default_location = TreeForeignKey(
|
||||||
@ -870,18 +872,18 @@ class Part(MPTTModel):
|
|||||||
help_text=_('Part notes - supports Markdown formatting')
|
help_text=_('Part notes - supports Markdown formatting')
|
||||||
)
|
)
|
||||||
|
|
||||||
bom_checksum = models.CharField(max_length=128, blank=True, help_text=_('Stored BOM checksum'))
|
bom_checksum = models.CharField(max_length=128, blank=True, verbose_name=_('BOM checksum'), help_text=_('Stored BOM checksum'))
|
||||||
|
|
||||||
bom_checked_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
|
bom_checked_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
related_name='boms_checked')
|
verbose_name=_('BOM checked by'), related_name='boms_checked')
|
||||||
|
|
||||||
bom_checked_date = models.DateField(blank=True, null=True)
|
bom_checked_date = models.DateField(blank=True, null=True, verbose_name=_('BOM checked date'))
|
||||||
|
|
||||||
creation_date = models.DateField(auto_now_add=True, editable=False, blank=True, null=True)
|
creation_date = models.DateField(auto_now_add=True, editable=False, blank=True, null=True, verbose_name=_('Creation Date'))
|
||||||
|
|
||||||
creation_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name='parts_created')
|
creation_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Creation User'), related_name='parts_created')
|
||||||
|
|
||||||
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name='parts_responible')
|
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Responsible'), related_name='parts_responible')
|
||||||
|
|
||||||
def format_barcode(self, **kwargs):
|
def format_barcode(self, **kwargs):
|
||||||
""" Return a JSON string for formatting a barcode for this Part object """
|
""" Return a JSON string for formatting a barcode for this Part object """
|
||||||
@ -1851,7 +1853,7 @@ class PartAttachment(InvenTreeAttachment):
|
|||||||
return os.path.join("part_files", str(self.part.id))
|
return os.path.join("part_files", str(self.part.id))
|
||||||
|
|
||||||
part = models.ForeignKey(Part, on_delete=models.CASCADE,
|
part = models.ForeignKey(Part, on_delete=models.CASCADE,
|
||||||
related_name='attachments')
|
verbose_name=_('Part'), related_name='attachments')
|
||||||
|
|
||||||
|
|
||||||
class PartSellPriceBreak(common.models.PriceBreak):
|
class PartSellPriceBreak(common.models.PriceBreak):
|
||||||
@ -1862,7 +1864,8 @@ class PartSellPriceBreak(common.models.PriceBreak):
|
|||||||
part = models.ForeignKey(
|
part = models.ForeignKey(
|
||||||
Part, on_delete=models.CASCADE,
|
Part, on_delete=models.CASCADE,
|
||||||
related_name='salepricebreaks',
|
related_name='salepricebreaks',
|
||||||
limit_choices_to={'salable': True}
|
limit_choices_to={'salable': True},
|
||||||
|
verbose_name=_('Part')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1880,9 +1883,9 @@ class PartStar(models.Model):
|
|||||||
user: Link to a User object
|
user: Link to a User object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='starred_users')
|
part = models.ForeignKey(Part, on_delete=models.CASCADE, verbose_name=_('Part'), related_name='starred_users')
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='starred_parts')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('User'), related_name='starred_parts')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['part', 'user']
|
unique_together = ['part', 'user']
|
||||||
@ -1955,6 +1958,7 @@ class PartTestTemplate(models.Model):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='test_templates',
|
related_name='test_templates',
|
||||||
limit_choices_to={'trackable': True},
|
limit_choices_to={'trackable': True},
|
||||||
|
verbose_name=_('Part'),
|
||||||
)
|
)
|
||||||
|
|
||||||
test_name = models.CharField(
|
test_name = models.CharField(
|
||||||
@ -2022,9 +2026,9 @@ class PartParameterTemplate(models.Model):
|
|||||||
except PartParameterTemplate.DoesNotExist:
|
except PartParameterTemplate.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
name = models.CharField(max_length=100, help_text=_('Parameter Name'), unique=True)
|
name = models.CharField(max_length=100, verbose_name=_('Name'), help_text=_('Parameter Name'), unique=True)
|
||||||
|
|
||||||
units = models.CharField(max_length=25, help_text=_('Parameter Units'), blank=True)
|
units = models.CharField(max_length=25, verbose_name=_('Units'), help_text=_('Parameter Units'), blank=True)
|
||||||
|
|
||||||
|
|
||||||
class PartParameter(models.Model):
|
class PartParameter(models.Model):
|
||||||
@ -2050,11 +2054,11 @@ class PartParameter(models.Model):
|
|||||||
# Prevent multiple instances of a parameter for a single part
|
# Prevent multiple instances of a parameter for a single part
|
||||||
unique_together = ('part', 'template')
|
unique_together = ('part', 'template')
|
||||||
|
|
||||||
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters', help_text=_('Parent Part'))
|
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters', verbose_name=_('Part'), help_text=_('Parent Part'))
|
||||||
|
|
||||||
template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE, related_name='instances', help_text=_('Parameter Template'))
|
template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE, related_name='instances', verbose_name=_('Template'), help_text=_('Parameter Template'))
|
||||||
|
|
||||||
data = models.CharField(max_length=500, help_text=_('Parameter Value'))
|
data = models.CharField(max_length=500, verbose_name=_('Data'), help_text=_('Parameter Value'))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, part, template, data, save=False):
|
def create(cls, part, template, data, save=False):
|
||||||
@ -2095,15 +2099,18 @@ class PartCategoryParameterTemplate(models.Model):
|
|||||||
category = models.ForeignKey(PartCategory,
|
category = models.ForeignKey(PartCategory,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='parameter_templates',
|
related_name='parameter_templates',
|
||||||
|
verbose_name=_('Category'),
|
||||||
help_text=_('Part Category'))
|
help_text=_('Part Category'))
|
||||||
|
|
||||||
parameter_template = models.ForeignKey(PartParameterTemplate,
|
parameter_template = models.ForeignKey(PartParameterTemplate,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='part_categories',
|
related_name='part_categories',
|
||||||
|
verbose_name=_('Parameter Template'),
|
||||||
help_text=_('Parameter Template'))
|
help_text=_('Parameter Template'))
|
||||||
|
|
||||||
default_value = models.CharField(max_length=500,
|
default_value = models.CharField(max_length=500,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name=_('Default Value'),
|
||||||
help_text=_('Default Parameter Value'))
|
help_text=_('Default Parameter Value'))
|
||||||
|
|
||||||
|
|
||||||
@ -2132,6 +2139,7 @@ class BomItem(models.Model):
|
|||||||
# A link to the parent part
|
# A link to the parent part
|
||||||
# Each part will get a reverse lookup field 'bom_items'
|
# Each part will get a reverse lookup field 'bom_items'
|
||||||
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items',
|
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items',
|
||||||
|
verbose_name=_('Part'),
|
||||||
help_text=_('Select parent part'),
|
help_text=_('Select parent part'),
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
'assembly': True,
|
'assembly': True,
|
||||||
@ -2140,26 +2148,28 @@ class BomItem(models.Model):
|
|||||||
# A link to the child item (sub-part)
|
# A link to the child item (sub-part)
|
||||||
# Each part will get a reverse lookup field 'used_in'
|
# Each part will get a reverse lookup field 'used_in'
|
||||||
sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in',
|
sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in',
|
||||||
|
verbose_name=_('Sub part'),
|
||||||
help_text=_('Select part to be used in BOM'),
|
help_text=_('Select part to be used in BOM'),
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
'component': True,
|
'component': True,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Quantity required
|
# Quantity required
|
||||||
quantity = models.DecimalField(default=1.0, max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], help_text=_('BOM quantity for this BOM item'))
|
quantity = models.DecimalField(default=1.0, max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], verbose_name=_('Quantity'), help_text=_('BOM quantity for this BOM item'))
|
||||||
|
|
||||||
optional = models.BooleanField(default=False, help_text=_("This BOM item is optional"))
|
optional = models.BooleanField(default=False, verbose_name=_('Optional'), help_text=_("This BOM item is optional"))
|
||||||
|
|
||||||
overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage],
|
overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage],
|
||||||
|
verbose_name=_('Overage'),
|
||||||
help_text=_('Estimated build wastage quantity (absolute or percentage)')
|
help_text=_('Estimated build wastage quantity (absolute or percentage)')
|
||||||
)
|
)
|
||||||
|
|
||||||
reference = models.CharField(max_length=500, blank=True, help_text=_('BOM item reference'))
|
reference = models.CharField(max_length=500, blank=True, verbose_name=_('Reference'), help_text=_('BOM item reference'))
|
||||||
|
|
||||||
# Note attached to this BOM line item
|
# Note attached to this BOM line item
|
||||||
note = models.CharField(max_length=500, blank=True, help_text=_('BOM item notes'))
|
note = models.CharField(max_length=500, blank=True, verbose_name=_('Note'), help_text=_('BOM item notes'))
|
||||||
|
|
||||||
checksum = models.CharField(max_length=128, blank=True, help_text=_('BOM line checksum'))
|
checksum = models.CharField(max_length=128, blank=True, verbose_name=_('Checksum'), help_text=_('BOM line checksum'))
|
||||||
|
|
||||||
inherited = models.BooleanField(
|
inherited = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
@ -2371,11 +2381,11 @@ class PartRelated(models.Model):
|
|||||||
""" Store and handle related parts (eg. mating connector, crimps, etc.) """
|
""" Store and handle related parts (eg. mating connector, crimps, etc.) """
|
||||||
|
|
||||||
part_1 = models.ForeignKey(Part, related_name='related_parts_1',
|
part_1 = models.ForeignKey(Part, related_name='related_parts_1',
|
||||||
on_delete=models.DO_NOTHING)
|
verbose_name=_('Part 1'), on_delete=models.DO_NOTHING)
|
||||||
|
|
||||||
part_2 = models.ForeignKey(Part, related_name='related_parts_2',
|
part_2 = models.ForeignKey(Part, related_name='related_parts_2',
|
||||||
on_delete=models.DO_NOTHING,
|
on_delete=models.DO_NOTHING,
|
||||||
help_text=_('Select Related Part'))
|
verbose_name=_('Part 2'), help_text=_('Select Related Part'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.part_1} <--> {self.part_2}'
|
return f'{self.part_1} <--> {self.part_2}'
|
||||||
|
@ -16,13 +16,13 @@
|
|||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class='alert alert-block alert-danger'>
|
<div class='alert alert-block alert-danger'>
|
||||||
The BOM for <i>{{ part.full_name }}</i> has changed, and must be validated.<br>
|
{% blocktrans with part=part.full_name %}The BOM for <i>{{ part }}</i> has changed, and must be validated.<br>{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
The BOM for <i>{{ part.full_name }}</i> was last checked by {{ part.bom_checked_by }} on {{ part.bom_checked_date }}
|
{% blocktrans with part=part.full_name checker=part.bom_checked_by check_date=part.bom_checked_date %}The BOM for <i>{{ part }}</i> was last checked by {{ checker }} on {{ check_date }}{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class='alert alert-danger alert-block'>
|
<div class='alert alert-danger alert-block'>
|
||||||
<b>The BOM for <i>{{ part.full_name }}</i> has not been validated.</b>
|
<b>{% blocktrans with part=part.full_name %}The BOM for <i>{{ part }}</i> has not been validated.{% endblocktrans %}</b>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||||
{{ col.name }}
|
{{ col.name }}
|
||||||
<button class='btn btn-default btn-remove' onClick='removeColFromBomWizard()' id='del_col_{{ forloop.counter0 }}' style='display: inline; float: right;' title='Remove column'>
|
<button class='btn btn-default btn-remove' onClick='removeColFromBomWizard()' id='del_col_{{ forloop.counter0 }}' style='display: inline; float: right;' title='{% trans "Remove column" %}'>
|
||||||
<span col_id='{{ forloop.counter0 }}' class='fas fa-trash-alt icon-red'></span>
|
<span col_id='{{ forloop.counter0 }}' class='fas fa-trash-alt icon-red'></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -73,7 +73,7 @@
|
|||||||
{% for row in bom_rows %}
|
{% for row in bom_rows %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ forloop.counter }}' style='display: inline; float: right;' title='Remove row'>
|
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ forloop.counter }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
|
||||||
<span row_id='{{ forloop.counter }}' class='fas fa-trash-alt icon-red'></span>
|
<span row_id='{{ forloop.counter }}' class='fas fa-trash-alt icon-red'></span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||||
<button type="submit" class="save btn btn-default">Upload File</button>
|
<button type="submit" class="save btn btn-default">{% trans 'Upload File' %}</button>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
{% extends "modal_form.html" %}
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
Confirm that the Bill of Materials (BOM) is valid for:<br><i>{{ part.full_name }}</i>
|
{% blocktrans with part.full_name as part %}Confirm that the Bill of Materials (BOM) is valid for:<br><i>{{ part }}</i>{% endblocktrans %}
|
||||||
|
|
||||||
<div class='alert alert-warning alert-block'>
|
<div class='alert alert-warning alert-block'>
|
||||||
This will validate each line in the BOM.
|
{% trans 'This will validate each line in the BOM.' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -65,8 +65,8 @@
|
|||||||
reload: true,
|
reload: true,
|
||||||
secondary: [{
|
secondary: [{
|
||||||
field: 'template',
|
field: 'template',
|
||||||
label: 'New Template',
|
label: '{% trans "New Template" %}',
|
||||||
title: 'Create New Parameter Template',
|
title: '{% trans "Create New Parameter Template" %}',
|
||||||
url: "{% url 'part-param-template-create' %}"
|
url: "{% url 'part-param-template-create' %}"
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
@ -246,7 +246,7 @@
|
|||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'part-pricing' part.id %}",
|
"{% url 'part-pricing' part.id %}",
|
||||||
{
|
{
|
||||||
submit_text: 'Calculate',
|
submit_text: '{% trans "Calculate" %}',
|
||||||
hideErrorMessage: true,
|
hideErrorMessage: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -4,33 +4,33 @@
|
|||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
<div class='alert alert-info alert-block'>
|
<div class='alert alert-info alert-block'>
|
||||||
Pricing information for:<br>
|
{% trans 'Pricing information for:' %}<br>
|
||||||
{{ part }}.
|
{{ part }}.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>Quantity</h4>
|
<h4>{% trans 'Quantity' %}</h4>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>Part</b></td>
|
<td><b>{% trans 'Part' %}</b></td>
|
||||||
<td colspan='2'>{{ part }}</td>
|
<td colspan='2'>{{ part }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>Quantity</b></td>
|
<td><b>{% trans 'Quantity' %}</b></td>
|
||||||
<td colspan='2'>{{ quantity }}</td>
|
<td colspan='2'>{{ quantity }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
{% if part.supplier_count > 0 %}
|
{% if part.supplier_count > 0 %}
|
||||||
<h4>Supplier Pricing</h4>
|
<h4>{% trans 'Supplier Pricing' %}</h4>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
{% if min_total_buy_price %}
|
{% if min_total_buy_price %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>Unit Cost</b></td>
|
<td><b>{% trans 'Unit Cost' %}</b></td>
|
||||||
<td>Min: {% include "price.html" with price=min_unit_buy_price %}</td>
|
<td>Min: {% include "price.html" with price=min_unit_buy_price %}</td>
|
||||||
<td>Max: {% include "price.html" with price=max_unit_buy_price %}</td>
|
<td>Max: {% include "price.html" with price=max_unit_buy_price %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if quantity > 1 %}
|
{% if quantity > 1 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>Total Cost</b></td>
|
<td><b>{% trans 'Total Cost' %}</b></td>
|
||||||
<td>Min: {% include "price.html" with price=min_total_buy_price %}</td>
|
<td>Min: {% include "price.html" with price=min_total_buy_price %}</td>
|
||||||
<td>Max: {% include "price.html" with price=max_total_buy_price %}</td>
|
<td>Max: {% include "price.html" with price=max_total_buy_price %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -38,7 +38,7 @@ Pricing information for:<br>
|
|||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan='3'>
|
<td colspan='3'>
|
||||||
<span class='warning-msg'><i>No supplier pricing available</i></span>
|
<span class='warning-msg'><i>{% trans 'No supplier pricing available' %}</i></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -46,17 +46,17 @@ Pricing information for:<br>
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if part.bom_count > 0 %}
|
{% if part.bom_count > 0 %}
|
||||||
<h4>BOM Pricing</h4>
|
<h4>{% trans 'BOM Pricing' %}</h4>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
{% if min_total_bom_price %}
|
{% if min_total_bom_price %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>Unit Cost</b></td>
|
<td><b>{% trans 'Unit Cost' %}</b></td>
|
||||||
<td>Min: {% include "price.html" with price=min_unit_bom_price %}</td>
|
<td>Min: {% include "price.html" with price=min_unit_bom_price %}</td>
|
||||||
<td>Max: {% include "price.html" with price=max_unit_bom_price %}</td>
|
<td>Max: {% include "price.html" with price=max_unit_bom_price %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if quantity > 1 %}
|
{% if quantity > 1 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>Total Cost</b></td>
|
<td><b>{% trans 'Total Cost' %}</b></td>
|
||||||
<td>Min: {% include "price.html" with price=min_total_bom_price %}</td>
|
<td>Min: {% include "price.html" with price=min_total_bom_price %}</td>
|
||||||
<td>Max: {% include "price.html" with price=max_total_bom_price %}</td>
|
<td>Max: {% include "price.html" with price=max_total_bom_price %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -64,14 +64,14 @@ Pricing information for:<br>
|
|||||||
{% if part.has_complete_bom_pricing == False %}
|
{% if part.has_complete_bom_pricing == False %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan='3'>
|
<td colspan='3'>
|
||||||
<span class='warning-msg'><i>Note: BOM pricing is incomplete for this part</i></span>
|
<span class='warning-msg'><i>{% trans 'Note: BOM pricing is incomplete for this part' %}</i></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan='3'>
|
<td colspan='3'>
|
||||||
<span class='warning-msg'><i>No BOM pricing available</i></span>
|
<span class='warning-msg'><i>{% trans 'No BOM pricing available' %}</i></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -151,7 +151,7 @@ Pricing information for:<br>
|
|||||||
{% if min_unit_buy_price or min_unit_bom_price %}
|
{% if min_unit_buy_price or min_unit_bom_price %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class='alert alert-danger alert-block'>
|
<div class='alert alert-danger alert-block'>
|
||||||
No pricing information is available for this part.
|
{% trans 'No pricing information is available for this part.' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
params: {
|
params: {
|
||||||
part: {{ part.id }},
|
part: {{ part.id }},
|
||||||
location_detail: true,
|
location_detail: true,
|
||||||
part_detail: true,
|
part_detail: false,
|
||||||
},
|
},
|
||||||
groupByField: 'location',
|
groupByField: 'location',
|
||||||
buttons: [
|
buttons: [
|
||||||
|
@ -325,3 +325,106 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(data['in_stock'], 1100)
|
self.assertEqual(data['in_stock'], 1100)
|
||||||
self.assertEqual(data['stock_item_count'], 105)
|
self.assertEqual(data['stock_item_count'], 105)
|
||||||
|
|
||||||
|
|
||||||
|
class PartParameterTest(InvenTreeAPITestCase):
|
||||||
|
"""
|
||||||
|
Tests for the ParParameter API
|
||||||
|
"""
|
||||||
|
|
||||||
|
superuser = True
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'location',
|
||||||
|
'params',
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def test_list_params(self):
|
||||||
|
"""
|
||||||
|
Test for listing part parameters
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = reverse('api-part-param-list')
|
||||||
|
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 5)
|
||||||
|
|
||||||
|
# Filter by part
|
||||||
|
response = self.client.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'part': 3,
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
# Filter by template
|
||||||
|
response = self.client.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'template': 1,
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
def test_create_param(self):
|
||||||
|
"""
|
||||||
|
Test that we can create a param via the API
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = reverse('api-part-param-list')
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'part': '2',
|
||||||
|
'template': '3',
|
||||||
|
'data': 70
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 6)
|
||||||
|
|
||||||
|
def test_param_detail(self):
|
||||||
|
"""
|
||||||
|
Tests for the PartParameter detail endpoint
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = reverse('api-part-param-detail', kwargs={'pk': 5})
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = response.data
|
||||||
|
|
||||||
|
self.assertEqual(data['pk'], 5)
|
||||||
|
self.assertEqual(data['part'], 3)
|
||||||
|
self.assertEqual(data['data'], '12')
|
||||||
|
|
||||||
|
# PATCH data back in
|
||||||
|
response = self.client.patch(url, {'data': '15'}, format='json')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Check that the data changed!
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
|
||||||
|
data = response.data
|
||||||
|
|
||||||
|
self.assertEqual(data['data'], '15')
|
||||||
|
@ -342,7 +342,7 @@ class PartSetCategory(AjaxUpdateView):
|
|||||||
|
|
||||||
data = {
|
data = {
|
||||||
'form_valid': valid,
|
'form_valid': valid,
|
||||||
'success': _('Set category for {n} parts'.format(n=len(self.parts)))
|
'success': _('Set category for {n} parts').format(n=len(self.parts))
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
|
@ -5,7 +5,7 @@ import logging
|
|||||||
import plugins.plugin as plugin
|
import plugins.plugin as plugin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class ActionPlugin(plugin.InvenTreePlugin):
|
class ActionPlugin(plugin.InvenTreePlugin):
|
||||||
|
@ -10,7 +10,7 @@ import plugins.action as action
|
|||||||
from plugins.action.action import ActionPlugin
|
from plugins.action.action import ActionPlugin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
def iter_namespace(pkg):
|
def iter_namespace(pkg):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.core.exceptions import ValidationError, FieldError
|
from django.core.exceptions import ValidationError, FieldError
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
@ -6,7 +6,7 @@ from django.apps import AppConfig
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class ReportConfig(AppConfig):
|
class ReportConfig(AppConfig):
|
||||||
|
35
InvenTree/report/migrations/0015_auto_20210403_1837.py
Normal file
35
InvenTree/report/migrations/0015_auto_20210403_1837.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-04-03 18:37
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import report.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('report', '0014_purchaseorderreport_salesorderreport'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='reportasset',
|
||||||
|
name='asset',
|
||||||
|
field=models.FileField(help_text='Report asset file', upload_to=report.models.rename_asset, verbose_name='Asset'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='reportasset',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(help_text='Asset file description', max_length=250, verbose_name='Description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='reportsnippet',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(help_text='Snippet file description', max_length=250, verbose_name='Description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='reportsnippet',
|
||||||
|
name='snippet',
|
||||||
|
field=models.FileField(help_text='Report snippet file', upload_to=report.models.rename_snippet, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm'])], verbose_name='Snippet'),
|
||||||
|
),
|
||||||
|
]
|
@ -38,7 +38,7 @@ except OSError as err:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class ReportFileUpload(FileSystemStorage):
|
class ReportFileUpload(FileSystemStorage):
|
||||||
@ -497,11 +497,12 @@ class ReportSnippet(models.Model):
|
|||||||
|
|
||||||
snippet = models.FileField(
|
snippet = models.FileField(
|
||||||
upload_to=rename_snippet,
|
upload_to=rename_snippet,
|
||||||
|
verbose_name=_('Snippet'),
|
||||||
help_text=_('Report snippet file'),
|
help_text=_('Report snippet file'),
|
||||||
validators=[FileExtensionValidator(allowed_extensions=['html', 'htm'])],
|
validators=[FileExtensionValidator(allowed_extensions=['html', 'htm'])],
|
||||||
)
|
)
|
||||||
|
|
||||||
description = models.CharField(max_length=250, help_text=_("Snippet file description"))
|
description = models.CharField(max_length=250, verbose_name=_('Description'), help_text=_("Snippet file description"))
|
||||||
|
|
||||||
|
|
||||||
def rename_asset(instance, filename):
|
def rename_asset(instance, filename):
|
||||||
@ -536,7 +537,8 @@ class ReportAsset(models.Model):
|
|||||||
|
|
||||||
asset = models.FileField(
|
asset = models.FileField(
|
||||||
upload_to=rename_asset,
|
upload_to=rename_asset,
|
||||||
|
verbose_name=_('Asset'),
|
||||||
help_text=_("Report asset file"),
|
help_text=_("Report asset file"),
|
||||||
)
|
)
|
||||||
|
|
||||||
description = models.CharField(max_length=250, help_text=_("Asset file description"))
|
description = models.CharField(max_length=250, verbose_name=_('Description'), help_text=_("Asset file description"))
|
||||||
|
@ -11,6 +11,7 @@ from django.conf.urls import url, include
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .models import StockLocation, StockItem
|
from .models import StockLocation, StockItem
|
||||||
from .models import StockItemTracking
|
from .models import StockItemTracking
|
||||||
@ -195,7 +196,7 @@ class StockCount(StockAdjust):
|
|||||||
if item['item'].stocktake(item['quantity'], request.user, notes=self.notes):
|
if item['item'].stocktake(item['quantity'], request.user, notes=self.notes):
|
||||||
n += 1
|
n += 1
|
||||||
|
|
||||||
return Response({'success': 'Updated stock for {n} items'.format(n=n)})
|
return Response({'success': _('Updated stock for {n} items').format(n=n)})
|
||||||
|
|
||||||
|
|
||||||
class StockAdd(StockAdjust):
|
class StockAdd(StockAdjust):
|
||||||
@ -264,7 +265,7 @@ class StockTransfer(StockAdjust):
|
|||||||
if item['item'].move(location, self.notes, request.user, quantity=item['quantity']):
|
if item['item'].move(location, self.notes, request.user, quantity=item['quantity']):
|
||||||
n += 1
|
n += 1
|
||||||
|
|
||||||
return Response({'success': 'Moved {n} parts to {loc}'.format(
|
return Response({'success': _('Moved {n} parts to {loc}').format(
|
||||||
n=n,
|
n=n,
|
||||||
loc=str(location),
|
loc=str(location),
|
||||||
)})
|
)})
|
||||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.utils import ErrorDict
|
from django.forms.utils import ErrorDict
|
||||||
from django.utils.translation import ugettext 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.core.exceptions import ValidationError
|
||||||
|
|
||||||
@ -111,10 +111,11 @@ class CreateStockItemForm(HelperForm):
|
|||||||
""" Form for creating a new StockItem """
|
""" Form for creating a new StockItem """
|
||||||
|
|
||||||
expiry_date = DatePickerFormField(
|
expiry_date = DatePickerFormField(
|
||||||
help_text=('Expiration date for this stock item'),
|
label=_('Expiry Date'),
|
||||||
|
help_text=_('Expiration date for this stock item'),
|
||||||
)
|
)
|
||||||
|
|
||||||
serial_numbers = forms.CharField(label=_('Serial numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)'))
|
serial_numbers = forms.CharField(label=_('Serial Numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)'))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -165,13 +166,13 @@ class CreateStockItemForm(HelperForm):
|
|||||||
class SerializeStockForm(HelperForm):
|
class SerializeStockForm(HelperForm):
|
||||||
""" Form for serializing a StockItem. """
|
""" Form for serializing a StockItem. """
|
||||||
|
|
||||||
destination = TreeNodeChoiceField(queryset=StockLocation.objects.all(), label='Destination', required=True, help_text='Destination for serialized stock (by default, will remain in current location)')
|
destination = TreeNodeChoiceField(queryset=StockLocation.objects.all(), label=_('Destination'), required=True, help_text=_('Destination for serialized stock (by default, will remain in current location)'))
|
||||||
|
|
||||||
serial_numbers = forms.CharField(label='Serial numbers', required=True, help_text='Unique serial numbers (must match quantity)')
|
serial_numbers = forms.CharField(label=_('Serial numbers'), required=True, help_text=_('Unique serial numbers (must match quantity)'))
|
||||||
|
|
||||||
note = forms.CharField(label='Notes', required=False, help_text='Add transaction note (optional)')
|
note = forms.CharField(label=_('Notes'), required=False, help_text=_('Add transaction note (optional)'))
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -263,7 +264,7 @@ class ExportOptionsForm(HelperForm):
|
|||||||
|
|
||||||
file_format = forms.ChoiceField(label=_('File Format'), help_text=_('Select output file format'))
|
file_format = forms.ChoiceField(label=_('File Format'), help_text=_('Select output file format'))
|
||||||
|
|
||||||
include_sublocations = forms.BooleanField(required=False, initial=True, help_text=_("Include stock items in sub locations"))
|
include_sublocations = forms.BooleanField(required=False, initial=True, label=_('Include sublocations'), help_text=_("Include stock items in sub locations"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StockLocation
|
model = StockLocation
|
||||||
@ -402,7 +403,8 @@ class EditStockItemForm(HelperForm):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
expiry_date = DatePickerFormField(
|
expiry_date = DatePickerFormField(
|
||||||
help_text=('Expiration date for this stock item'),
|
label=_('Expiry Date'),
|
||||||
|
help_text=_('Expiration date for this stock item'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
92
InvenTree/stock/migrations/0059_auto_20210404_2016.py
Normal file
92
InvenTree/stock/migrations/0059_auto_20210404_2016.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-04-04 20:16
|
||||||
|
|
||||||
|
import InvenTree.fields
|
||||||
|
import InvenTree.models
|
||||||
|
import InvenTree.validators
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0005_owner_model'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('stock', '0058_stockitem_packaging'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='delete_on_deplete',
|
||||||
|
field=models.BooleanField(default=True, help_text='Delete this Stock Item when stock is depleted', verbose_name='Delete on deplete'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Select Owner', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='users.Owner', verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemattachment',
|
||||||
|
name='comment',
|
||||||
|
field=models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemattachment',
|
||||||
|
name='upload_date',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True, verbose_name='upload date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemattachment',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemtracking',
|
||||||
|
name='link',
|
||||||
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external page for further information', verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemtracking',
|
||||||
|
name='notes',
|
||||||
|
field=models.CharField(blank=True, help_text='Entry notes', max_length=512, verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemtracking',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemtracking',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(help_text='Tracking entry title', max_length=250, verbose_name='Title'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stocklocation',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, help_text='Description (optional)', max_length=250, verbose_name='Description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stocklocation',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Name', max_length=100, validators=[InvenTree.validators.validate_tree_name], verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stocklocation',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Select Owner', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_locations', to='users.Owner', verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stocklocation',
|
||||||
|
name='parent',
|
||||||
|
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='stock.StockLocation', verbose_name='parent'),
|
||||||
|
),
|
||||||
|
]
|
@ -51,7 +51,8 @@ class StockLocation(InvenTreeTree):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, blank=True, null=True,
|
owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
help_text='Select Owner',
|
verbose_name=_('Owner'),
|
||||||
|
help_text=_('Select Owner'),
|
||||||
related_name='stock_locations')
|
related_name='stock_locations')
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@ -483,7 +484,7 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
review_needed = models.BooleanField(default=False)
|
review_needed = models.BooleanField(default=False)
|
||||||
|
|
||||||
delete_on_deplete = models.BooleanField(default=True, help_text=_('Delete this Stock Item when stock is depleted'))
|
delete_on_deplete = models.BooleanField(default=True, verbose_name=_('Delete on deplete'), help_text=_('Delete this Stock Item when stock is depleted'))
|
||||||
|
|
||||||
status = models.PositiveIntegerField(
|
status = models.PositiveIntegerField(
|
||||||
default=StockStatus.OK,
|
default=StockStatus.OK,
|
||||||
@ -507,7 +508,8 @@ class StockItem(MPTTModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, blank=True, null=True,
|
owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
help_text='Select Owner',
|
verbose_name=_('Owner'),
|
||||||
|
help_text=_('Select Owner'),
|
||||||
related_name='stock_items')
|
related_name='stock_items')
|
||||||
|
|
||||||
def is_stale(self):
|
def is_stale(self):
|
||||||
@ -948,7 +950,7 @@ class StockItem(MPTTModel):
|
|||||||
raise ValidationError({"quantity": _("Quantity must be greater than zero")})
|
raise ValidationError({"quantity": _("Quantity must be greater than zero")})
|
||||||
|
|
||||||
if quantity > self.quantity:
|
if quantity > self.quantity:
|
||||||
raise ValidationError({"quantity": _("Quantity must not exceed available stock quantity ({n})".format(n=self.quantity))})
|
raise ValidationError({"quantity": _("Quantity must not exceed available stock quantity ({n})").format(n=self.quantity)})
|
||||||
|
|
||||||
if not type(serials) in [list, tuple]:
|
if not type(serials) in [list, tuple]:
|
||||||
raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")})
|
raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")})
|
||||||
@ -989,7 +991,7 @@ class StockItem(MPTTModel):
|
|||||||
new_item.addTransactionNote(_('Add serial number'), user, notes=notes)
|
new_item.addTransactionNote(_('Add serial number'), user, notes=notes)
|
||||||
|
|
||||||
# Remove the equivalent number of items
|
# Remove the equivalent number of items
|
||||||
self.take_stock(quantity, user, notes=_('Serialized {n} items'.format(n=quantity)))
|
self.take_stock(quantity, user, notes=_('Serialized {n} items').format(n=quantity))
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def copyHistoryFrom(self, other):
|
def copyHistoryFrom(self, other):
|
||||||
@ -1548,17 +1550,17 @@ class StockItemTracking(models.Model):
|
|||||||
|
|
||||||
date = models.DateTimeField(auto_now_add=True, editable=False)
|
date = models.DateTimeField(auto_now_add=True, editable=False)
|
||||||
|
|
||||||
title = models.CharField(blank=False, max_length=250, help_text=_('Tracking entry title'))
|
title = models.CharField(blank=False, max_length=250, verbose_name=_('Title'), help_text=_('Tracking entry title'))
|
||||||
|
|
||||||
notes = models.CharField(blank=True, max_length=512, help_text=_('Entry notes'))
|
notes = models.CharField(blank=True, max_length=512, verbose_name=_('Notes'), help_text=_('Entry notes'))
|
||||||
|
|
||||||
link = InvenTreeURLField(blank=True, help_text=_('Link to external page for further information'))
|
link = InvenTreeURLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page for further information'))
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
|
|
||||||
system = models.BooleanField(default=False)
|
system = models.BooleanField(default=False)
|
||||||
|
|
||||||
quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)
|
quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'))
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
# image = models.ImageField(upload_to=func, max_length=255, null=True, blank=True)
|
# image = models.ImageField(upload_to=func, max_length=255, null=True, blank=True)
|
||||||
|
@ -165,13 +165,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
{% if item.in_stock %}
|
{% if item.in_stock %}
|
||||||
<li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
|
<li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.in_stock and item.can_adjust_location %}
|
|
||||||
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.in_stock and item.part.trackable %}
|
{% if item.in_stock and item.part.trackable %}
|
||||||
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
|
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if item.in_stock and item.can_adjust_location %}
|
||||||
|
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
{% if item.in_stock and item.can_adjust_location and item.part.salable and not item.customer %}
|
{% if item.in_stock and item.can_adjust_location and item.part.salable and not item.customer %}
|
||||||
<li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
<li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -451,7 +451,7 @@ $("#stock-edit").click(function () {
|
|||||||
"{% url 'stock-item-edit' item.id %}",
|
"{% url 'stock-item-edit' item.id %}",
|
||||||
{
|
{
|
||||||
reload: true,
|
reload: true,
|
||||||
submit_text: "Save",
|
submit_text: '{% trans "Save" %}',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -160,7 +160,7 @@
|
|||||||
|
|
||||||
$("#stock-export").click(function() {
|
$("#stock-export").click(function() {
|
||||||
launchModalForm("{% url 'stock-export-options' %}", {
|
launchModalForm("{% url 'stock-export-options' %}", {
|
||||||
submit_text: "Export",
|
submit_text: '{% trans "Export" %}',
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
var url = "{% url 'stock-export' %}";
|
var url = "{% url 'stock-export' %}";
|
||||||
|
|
||||||
@ -188,8 +188,8 @@
|
|||||||
secondary: [
|
secondary: [
|
||||||
{
|
{
|
||||||
field: 'parent',
|
field: 'parent',
|
||||||
label: 'New Location',
|
label: '{% trans "New Location" %}',
|
||||||
title: 'Create new location',
|
title: '{% trans "Create new location" %}',
|
||||||
url: "{% url 'stock-location-create' %}",
|
url: "{% url 'stock-location-create' %}",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<input type='hidden' name='stock-id-{{ item.id }}' value='{{ item.new_quantity }}'/>
|
<input type='hidden' name='stock-id-{{ item.id }}' value='{{ item.new_quantity }}'/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><button class='btn btn-default btn-remove' onclick='removeStockRow()' id='del-{{ item.id }}' title='Remove item' type='button'><span row='stock-row-{{ item.id }}' class='fas fa-trash-alt icon-red'></span></button></td>
|
<td><button class='btn btn-default btn-remove' onclick='removeStockRow()' id='del-{{ item.id }}' title='{% trans "Remove item" %}' type='button'><span row='stock-row-{{ item.id }}' class='fas fa-trash-alt icon-red'></span></button></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -14,7 +14,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 django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
|
|
||||||
@ -965,7 +965,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
|
|
||||||
context['stock_action'] = self.stock_action.strip().lower()
|
context['stock_action'] = self.stock_action.strip().lower()
|
||||||
|
|
||||||
context['stock_action_title'] = self.stock_action.capitalize()
|
context['stock_action_title'] = self.stock_action_title
|
||||||
|
|
||||||
# Quantity column will be read-only in some circumstances
|
# Quantity column will be read-only in some circumstances
|
||||||
context['edit_quantity'] = not self.stock_action == 'delete'
|
context['edit_quantity'] = not self.stock_action == 'delete'
|
||||||
@ -993,16 +993,17 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
if self.stock_action not in ['move', 'count', 'take', 'add', 'delete']:
|
if self.stock_action not in ['move', 'count', 'take', 'add', 'delete']:
|
||||||
self.stock_action = 'count'
|
self.stock_action = 'count'
|
||||||
|
|
||||||
# Choose the form title based on the action
|
# Choose form title and action column based on the action
|
||||||
titles = {
|
titles = {
|
||||||
'move': _('Move Stock Items'),
|
'move': [_('Move Stock Items'), _('Move')],
|
||||||
'count': _('Count Stock Items'),
|
'count': [_('Count Stock Items'), _('Count')],
|
||||||
'take': _('Remove From Stock'),
|
'take': [_('Remove From Stock'), _('Take')],
|
||||||
'add': _('Add Stock Items'),
|
'add': [_('Add Stock Items'), _('Add')],
|
||||||
'delete': _('Delete Stock Items')
|
'delete': [_('Delete Stock Items'), _('Delete')],
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ajax_form_title = titles[self.stock_action]
|
self.ajax_form_title = titles[self.stock_action][0]
|
||||||
|
self.stock_action_title = titles[self.stock_action][1]
|
||||||
|
|
||||||
# Save list of items!
|
# Save list of items!
|
||||||
self.stock_items = self.get_GET_items()
|
self.stock_items = self.get_GET_items()
|
||||||
@ -1039,7 +1040,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
if self.stock_action in ['move', 'take']:
|
if self.stock_action in ['move', 'take']:
|
||||||
|
|
||||||
if item.new_quantity > item.quantity:
|
if item.new_quantity > item.quantity:
|
||||||
item.error = _('Quantity must not exceed {x}'.format(x=item.quantity))
|
item.error = _('Quantity must not exceed {x}').format(x=item.quantity)
|
||||||
valid = False
|
valid = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -1118,7 +1119,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
return f"{_('Added stock to ')} {count} {_('items')}"
|
return _('Added stock to {n} items').format(n=count)
|
||||||
|
|
||||||
def do_take(self):
|
def do_take(self):
|
||||||
|
|
||||||
@ -1133,7 +1134,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
return f"{_('Removed stock from ')} {count} {_('items')}"
|
return _('Removed stock from {n} items').format(n=count)
|
||||||
|
|
||||||
def do_count(self):
|
def do_count(self):
|
||||||
|
|
||||||
@ -1189,9 +1190,9 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
return _('No items were moved')
|
return _('No items were moved')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return _('Moved {n} items to {dest}'.format(
|
return _('Moved {n} items to {dest}').format(
|
||||||
n=count,
|
n=count,
|
||||||
dest=destination.pathstring))
|
dest=destination.pathstring)
|
||||||
|
|
||||||
def do_delete(self):
|
def do_delete(self):
|
||||||
""" Delete multiple stock items """
|
""" Delete multiple stock items """
|
||||||
@ -1208,7 +1209,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
return _("Deleted {n} stock items".format(n=count))
|
return _("Deleted {n} stock items").format(n=count)
|
||||||
|
|
||||||
|
|
||||||
class StockItemEdit(AjaxUpdateView):
|
class StockItemEdit(AjaxUpdateView):
|
||||||
|
@ -131,6 +131,7 @@ addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa
|
|||||||
|
|
||||||
loadStockTable($('#table-recently-updated-stock'), {
|
loadStockTable($('#table-recently-updated-stock'), {
|
||||||
params: {
|
params: {
|
||||||
|
part_detail: true,
|
||||||
ordering: "-updated",
|
ordering: "-updated",
|
||||||
max_results: {% settings_value "STOCK_RECENT_COUNT" %},
|
max_results: {% settings_value "STOCK_RECENT_COUNT" %},
|
||||||
},
|
},
|
||||||
|
@ -133,14 +133,14 @@ InvenTree | {% trans "Search Results" %}
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
title: 'Name',
|
title: '{% trans "Name" %}',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return renderLink(value, '/part/category/' + row.pk + '/');
|
return renderLink(value, '/part/category/' + row.pk + '/');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
title: 'Description',
|
title: '{% trans "Description" %}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -270,14 +270,14 @@ InvenTree | {% trans "Search Results" %}
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
title: 'Name',
|
title: '{% trans "Name" %}',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return renderLink(row.pathstring, '/stock/location/' + row.pk + '/');
|
return renderLink(row.pathstring, '/stock/location/' + row.pk + '/');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
title: 'Description',
|
title: '{% trans "Description" %}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<span class='fas {{ icon }}'></span>
|
<span class='fas {{ icon }}'></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><b>{{ setting.name }}</b></td>
|
<td><b>{% trans setting.name %}</b></td>
|
||||||
<td>
|
<td>
|
||||||
{% if setting.is_bool %}
|
{% if setting.is_bool %}
|
||||||
<div>
|
<div>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{{ setting.description }}
|
{% trans setting.description %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class='btn-group float-right'>
|
<div class='btn-group float-right'>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user