Merge remote-tracking branch 'upstream/master' into django-q

# Conflicts:
#	InvenTree/InvenTree/status.py
#	InvenTree/templates/about.html
This commit is contained in:
Oliver Walters 2021-04-07 22:29:47 +10:00
commit 9c38d67b52
76 changed files with 4616 additions and 2335 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,11 @@ Provides system status functionality checks.
""" """
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.utils.translation import ugettext as _
from django_q.models import Success from django_q.models import Success
from django_q.monitor import Stat from django_q.monitor import Stat

View File

@ -1,4 +1,4 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
class StatusCode: class StatusCode:

View File

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

View File

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

View File

@ -71,7 +71,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
@ -81,5 +81,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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -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
@ -114,7 +114,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 +122,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 +367,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 +531,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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -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(
@ -434,13 +437,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,
@ -448,13 +452,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
@ -585,11 +590,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):
@ -615,6 +620,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')
) )
@ -628,10 +634,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,
@ -657,9 +664,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 = [
@ -759,7 +766,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',
@ -770,10 +777,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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -1057,7 +1057,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)
@ -1348,7 +1348,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)

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,35 +1,37 @@
{% extends "modal_form.html" %} {% extends "modal_form.html" %}
{% load i18n %}
{% 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>
@ -37,7 +39,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 %}
@ -45,17 +47,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>
@ -63,14 +65,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 %}
@ -80,7 +82,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 %}

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,16 +34,20 @@
<td>{% trans "Django Version" %}</td> <td>{% trans "Django Version" %}</td>
<td><a href="https://www.djangoproject.com/">{% django_version %}</a></td> <td><a href="https://www.djangoproject.com/">{% django_version %}</a></td>
</tr> </tr>
{% inventree_commit_hash as hash %}
{% if hash %}
<tr> <tr>
<td><span class='fas fa-code-branch'></span></td> <td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Commit Hash" %}</td> <td>{% trans "Commit Hash" %}</td><td>{{ hash }}</td>
<td><a href="https://github.com/inventree/InvenTree/commit/{% inventree_commit_hash %}">{% inventree_commit_hash %}</a></td>
</tr> </tr>
{% endif %}
{% inventree_commit_date as commit_date %}
{% if commit_date %}
<tr> <tr>
<td><span class='fas fa-calendar-alt'></span></td> <td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Commit Date" %}</td> <td>{% trans "Commit Date" %}</td><td>{{ commit_date }}</td>
<td>{% inventree_commit_date %}</td>
</tr> </tr>
{% endif %}
<tr> <tr>
<td><span class='fas fa-book'></span></td> <td><span class='fas fa-book'></span></td>
<td>{% trans "InvenTree Documentation" %}</td> <td>{% trans "InvenTree Documentation" %}</td>

View File

@ -134,12 +134,12 @@ InvenTree
<script type='text/javascript' src='{% static "bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js" %}'></script> <script type='text/javascript' src='{% static "bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js" %}'></script>
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script> <script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script> <script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script> <script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/filters.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
@ -156,6 +156,7 @@ InvenTree
<script type='text/javascript' src="{% url 'calendar.js' %}"></script> <script type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script type='text/javascript' src="{% url 'tables.js' %}"></script> <script type='text/javascript' src="{% url 'tables.js' %}"></script>
<script type='text/javascript' src="{% url 'table_filters.js' %}"></script> <script type='text/javascript' src="{% url 'table_filters.js' %}"></script>
<script type='text/javascript' src="{% url 'filters.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script> <script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script> <script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
@ -180,6 +181,7 @@ $(document).ready(function () {
}); });
{% endif %} {% endif %}
moment.locale('{{request.LANGUAGE_CODE}}');
}); });
</script> </script>

View File

@ -1,3 +1,5 @@
{% load i18n %}
/** /**
* Code for managing query filters / table options. * Code for managing query filters / table options.
* *
@ -188,7 +190,7 @@ function generateAvailableFilterList(tableKey) {
var html = `<select class='form-control filter-input' id='${id}' name='tag'>`; var html = `<select class='form-control filter-input' id='${id}' name='tag'>`;
html += `<option value=''>Select filter</option>`; html += "<option value=''>{% trans 'Select filter' %}</option>";
for (var opt in remaining) { for (var opt in remaining) {
var title = getFilterTitle(tableKey, opt); var title = getFilterTitle(tableKey, opt);
@ -263,10 +265,10 @@ function setupFilterList(tableKey, table, target) {
// One blank slate, please // One blank slate, please
element.empty(); element.empty();
element.append(`<button id='${add}' title='Add new filter' class='btn btn-default filter-tag'><span class='fas fa-filter'></span></button>`); element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-default filter-tag'><span class='fas fa-filter'></span></button>`);
if (Object.keys(filters).length > 0) { if (Object.keys(filters).length > 0) {
element.append(`<button id='${clear}' title='Clear all filters' class='btn btn-default filter-tag'><span class='fas fa-trash-alt'></span></button>`); element.append(`<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-default filter-tag'><span class='fas fa-trash-alt'></span></button>`);
} }
for (var key in filters) { for (var key in filters) {
@ -291,7 +293,7 @@ function setupFilterList(tableKey, table, target) {
html += generateAvailableFilterList(tableKey); html += generateAvailableFilterList(tableKey);
html += generateFilterInput(tableKey); html += generateFilterInput(tableKey);
html += `<button title='Create filter' class='btn btn-default filter-tag' id='${make}'><span class='fas fa-plus'></span></button>`; html += `<button title='{% trans "Create filter" %}' class='btn btn-default filter-tag' id='${make}'><span class='fas fa-plus'></span></button>`;
//html += '</div>'; //html += '</div>';

View File

@ -253,7 +253,7 @@ function loadingMessageContent() {
*/ */
// TODO - This can be made a lot better // TODO - This can be made a lot better
return "<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> Waiting for server..."; return "<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> {% trans 'Waiting for server...' %}";
} }

View File

@ -976,8 +976,8 @@ function loadStockTrackingTable(table, options) {
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
// Manually created entries can be edited or deleted // Manually created entries can be edited or deleted
if (!row.system) { if (!row.system) {
var bEdit = "<button title='Edit tracking entry' class='btn btn-entry-edit btn-default btn-glyph' type='button' url='/stock/track/" + row.pk + "/edit/'><span class='fas fa-edit'/></button>"; var bEdit = "<button title='{% trans 'Edit tracking entry' %}' class='btn btn-entry-edit btn-default btn-glyph' type='button' url='/stock/track/" + row.pk + "/edit/'><span class='fas fa-edit'/></button>";
var bDel = "<button title='Delete tracking entry' class='btn btn-entry-delete btn-default btn-glyph' type='button' url='/stock/track/" + row.pk + "/delete/'><span class='fas fa-trash-alt icon-red'/></button>"; var bDel = "<button title='{% trans 'Delete tracking entry' %}' class='btn btn-entry-delete btn-default btn-glyph' type='button' url='/stock/track/" + row.pk + "/delete/'><span class='fas fa-trash-alt icon-red'/></button>";
return "<div class='btn-group' role='group'>" + bEdit + bDel + "</div>"; return "<div class='btn-group' role='group'>" + bEdit + bDel + "</div>";
} else { } else {

View File

@ -1 +1 @@
<button type='button' class='btn btn-default' id='show-qr-code' title='Show QR code'><span class='fas fa-qrcode'></span></button> <button type='button' class='btn btn-default' id='show-qr-code' title='{% trans "Show QR Code" %}'><span class='fas fa-qrcode'></span></button>