diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 231423be8f..5ede677b9f 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -52,6 +52,24 @@ def str2bool(text, test=True): return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ] +def decimal2string(d): + """ + Format a Decimal number as a string, + stripping out any trailing zeroes or decimal points. + Essentially make it look like a whole number if it is one. + + Args: + d: A python Decimal object + + Returns: + A string representation of the input number + """ + + s = str(d) + + return s.rstrip("0").rstrip(".") + + def WrapWithQuotes(text, quote='"'): """ Wrap the supplied text with quotes diff --git a/InvenTree/InvenTree/static/script/inventree/bom.js b/InvenTree/InvenTree/static/script/inventree/bom.js index 97dca94017..e62c8c22f4 100644 --- a/InvenTree/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/InvenTree/static/script/inventree/bom.js @@ -163,21 +163,16 @@ function loadBomTable(table, options) { formatter: function(value, row, index, field) { var text = value; + // The 'value' is a text string with (potentially) multiple trailing zeros + // Let's make it a bit more pretty + text = parseFloat(text); + if (row.overage) { text += " (+" + row.overage + ") "; } return text; }, - footerFormatter: function(data) { - var quantity = 0; - - data.forEach(function(item) { - quantity += item.quantity; - }); - - return quantity; - }, }); if (!options.editable) { diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index 0904c22a0d..8ad934760b 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -183,6 +183,11 @@ function loadPartTable(table, url, options={}) { sortable: true, formatter: function(value, row, index, field) { if (value) { + + if (row.units) { + value += ' ' + row.units + ''; + } + return renderLink(value, '/part/' + row.pk + '/stock/'); } else { diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index 4fffc961c7..612c8b5c88 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -80,6 +80,8 @@ function loadStockTable(table, options) { items += 1; }); + stock = +stock.toFixed(5); + return stock + " (" + items + " items)"; } else if (field == 'batch') { var batches = []; diff --git a/InvenTree/build/migrations/0007_auto_20191118_2321.py b/InvenTree/build/migrations/0007_auto_20191118_2321.py new file mode 100644 index 0000000000..da6b455f60 --- /dev/null +++ b/InvenTree/build/migrations/0007_auto_20191118_2321.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.5 on 2019-11-18 23:21 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0006_auto_20190913_1407'), + ] + + operations = [ + 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(1)]), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 57792340e8..51e1b7f5e7 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -48,7 +48,7 @@ class Build(models.Model): title = models.CharField( blank=False, max_length=100, - help_text='Brief description of the build') + help_text=_('Brief description of the build')) part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='builds', @@ -57,28 +57,28 @@ class Build(models.Model): 'assembly': True, 'active': True }, - help_text='Select part to build', + help_text=_('Select part to build'), ) take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL, related_name='sourcing_builds', null=True, blank=True, - help_text='Select location to take stock from for this build (leave blank to take from any stock location)' + help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)') ) quantity = models.PositiveIntegerField( default=1, validators=[MinValueValidator(1)], - help_text='Number of parts to build' + help_text=_('Number of parts to build') ) status = models.PositiveIntegerField(default=BuildStatus.PENDING, choices=BuildStatus.items(), validators=[MinValueValidator(0)], - help_text='Build status') + help_text=_('Build status')) batch = models.CharField(max_length=100, blank=True, null=True, - help_text='Batch code for this build output') + help_text=_('Batch code for this build output')) creation_date = models.DateField(auto_now=True, editable=False) @@ -90,10 +90,9 @@ class Build(models.Model): related_name='builds_completed' ) - URL = InvenTreeURLField(blank=True, help_text='Link to external URL') + URL = InvenTreeURLField(blank=True, help_text=_('Link to external URL')) - notes = models.TextField(blank=True, help_text='Extra build notes') - """ Notes attached to each build output """ + notes = models.TextField(blank=True, help_text=_('Extra build notes')) @transaction.atomic def cancelBuild(self, user): @@ -399,18 +398,20 @@ class BuildItem(models.Model): Build, on_delete=models.CASCADE, related_name='allocated_stock', - help_text='Build to allocate parts' + help_text=_('Build to allocate parts') ) stock_item = models.ForeignKey( 'stock.StockItem', on_delete=models.CASCADE, related_name='allocations', - help_text='Stock Item to allocate to build', + help_text=_('Stock Item to allocate to build'), ) - quantity = models.PositiveIntegerField( + quantity = models.DecimalField( + decimal_places=5, + max_digits=15, default=1, validators=[MinValueValidator(1)], - help_text='Stock quantity to allocate to build' + help_text=_('Stock quantity to allocate to build') ) diff --git a/InvenTree/build/templates/build/allocate_edit.html b/InvenTree/build/templates/build/allocate_edit.html index d5978afa1d..ca6990ee00 100644 --- a/InvenTree/build/templates/build/allocate_edit.html +++ b/InvenTree/build/templates/build/allocate_edit.html @@ -1,12 +1,14 @@ +{% load i18n %} +{% load inventree_extras %}
Part | -Description | -Available | -Required | -Allocated | -On Order | +{% trans "Part" %} | +{% trans "Description" %} | +{% trans "Available" %} | +{% trans "Required" %} | +{% trans "Allocated" %} | +{% trans "On Order" %} | {{ item.part.description }} | -{{ item.part.total_stock }} | -{{ item.quantity }} | +{% decimal item.part.total_stock %} | +{% decimal item.quantity %} | {{ item.allocated }} | -{{ item.part.on_order }} | +{% decimal item.part.on_order %} | {% endfor %} diff --git a/InvenTree/company/migrations/0009_auto_20191118_2323.py b/InvenTree/company/migrations/0009_auto_20191118_2323.py new file mode 100644 index 0000000000..0ad6c682f2 --- /dev/null +++ b/InvenTree/company/migrations/0009_auto_20191118_2323.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.5 on 2019-11-18 23:23 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0008_auto_20190913_1407'), + ] + + operations = [ + migrations.AlterField( + model_name='supplierpricebreak', + name='quantity', + field=models.DecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(1)]), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index d1c340921a..f16902f783 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -379,7 +379,7 @@ class SupplierPriceBreak(models.Model): part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks') - quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)]) + quantity = models.DecimalField(max_digits=15, decimal_places=5, default=1, validators=[MinValueValidator(1)]) cost = models.DecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)]) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 5d59143a69..cb1f0d359c 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-11-16 09:28+0000\n" +"POT-Creation-Date: 2019-11-18 23:32+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME
---|
Supplier | +{% trans "Supplier" %} | {{ order.supplier }} | ||||||||||||||
Status | +{% trans "Status" %} | {% include "order/order_status.html" %} | ||||||||||||||
Created | +{% trans "Created" %} | {{ order.creation_date }}{{ order.created_by }} | ||||||||||||||
Issued | +{% trans "Issued" %} | {{ order.issue_date }} | ||||||||||||||
Received | +{% trans "Received" %} | {{ order.complete_date }}{{ order.received_by }} |
Line | -Part | -Description | -Order Code | -Reference | -Quantity | +{% trans "Line" %} | +{% trans "Part" %} | +{% trans "Description" %} | +{% trans "Order Code" %} | +{% trans "Reference" %} | +{% trans "Quantity" %} | {% if not order.status == OrderStatus.PENDING %} -Received | +{% trans "Received" %} | {% endif %} -Note | +{% trans "Note" %} | Warning: Part has been deleted. | {% endif %}{{ line.reference }} | -{{ line.quantity }} | +{% decimal line.quantity %} | {% if not order.status == OrderStatus.PENDING %} -{{ line.received }} | +{% decimal line.received %} | {% endif %}
{{ line.notes }}
@@ -160,7 +162,7 @@ InvenTree | {{ order }}
{% if order.notes %}
-
{% endif %}
diff --git a/InvenTree/part/migrations/0024_auto_20191118_2139.py b/InvenTree/part/migrations/0024_auto_20191118_2139.py
new file mode 100644
index 0000000000..95892993b2
--- /dev/null
+++ b/InvenTree/part/migrations/0024_auto_20191118_2139.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.5 on 2019-11-18 21:39
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0023_auto_20190913_1401'),
+ ]
+
+ operations = [
+ 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)]),
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0025_auto_20191118_2316.py b/InvenTree/part/migrations/0025_auto_20191118_2316.py
new file mode 100644
index 0000000000..cbdd7be75a
--- /dev/null
+++ b/InvenTree/part/migrations/0025_auto_20191118_2316.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.5 on 2019-11-18 23:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0024_auto_20191118_2139'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='part',
+ name='units',
+ field=models.CharField(blank=True, default='', help_text='Stock keeping units for this part', max_length=20),
+ ),
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index a06d5ea718..809ef0a428 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -53,10 +53,10 @@ class PartCategory(InvenTreeTree):
'stock.StockLocation', related_name="default_categories",
null=True, blank=True,
on_delete=models.SET_NULL,
- help_text='Default location for parts in this category'
+ help_text=_('Default location for parts in this category')
)
- default_keywords = models.CharField(blank=True, max_length=250, help_text='Default keywords for parts in this category')
+ default_keywords = models.CharField(blank=True, max_length=250, help_text=_('Default keywords for parts in this category'))
def get_absolute_url(self):
return reverse('category-detail', kwargs={'pk': self.id})
@@ -324,11 +324,11 @@ class Part(models.Model):
})
name = models.CharField(max_length=100, blank=False,
- help_text='Part name',
+ help_text=_('Part name'),
validators=[validators.validate_part_name]
)
- is_template = models.BooleanField(default=False, help_text='Is this part a template part?')
+ is_template = models.BooleanField(default=False, help_text=_('Is this part a template part?'))
variant_of = models.ForeignKey('part.Part', related_name='variants',
null=True, blank=True,
@@ -337,28 +337,28 @@ class Part(models.Model):
'active': True,
},
on_delete=models.SET_NULL,
- help_text='Is this part a variant of another part?')
+ help_text=_('Is this part a variant of another part?'))
- description = models.CharField(max_length=250, blank=False, help_text='Part description')
+ description = models.CharField(max_length=250, blank=False, help_text=_('Part description'))
- keywords = models.CharField(max_length=250, blank=True, help_text='Part keywords to improve visibility in search results')
+ keywords = models.CharField(max_length=250, blank=True, help_text=_('Part keywords to improve visibility in search results'))
category = TreeForeignKey(PartCategory, related_name='parts',
null=True, blank=True,
on_delete=models.DO_NOTHING,
- help_text='Part category')
+ help_text=_('Part category'))
- IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
+ IPN = models.CharField(max_length=100, blank=True, help_text=_('Internal Part Number'))
- revision = models.CharField(max_length=100, blank=True, help_text='Part revision or version number')
+ revision = models.CharField(max_length=100, blank=True, help_text=_('Part revision or version number'))
- URL = InvenTreeURLField(blank=True, help_text='Link to extenal URL')
+ URL = InvenTreeURLField(blank=True, help_text=_('Link to extenal URL'))
image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True)
default_location = TreeForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
blank=True, null=True,
- help_text='Where is this item normally stored?',
+ help_text=_('Where is this item normally stored?'),
related_name='default_parts')
def get_default_location(self):
@@ -402,30 +402,30 @@ class Part(models.Model):
default_supplier = models.ForeignKey(SupplierPart,
on_delete=models.SET_NULL,
blank=True, null=True,
- help_text='Default supplier part',
+ help_text=_('Default supplier part'),
related_name='default_parts')
- minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text='Minimum allowed stock level')
+ minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text=_('Minimum allowed stock level'))
- units = models.CharField(max_length=20, default="pcs", blank=True, help_text='Stock keeping units for this part')
+ units = models.CharField(max_length=20, default="", blank=True, help_text=_('Stock keeping units for this part'))
- assembly = models.BooleanField(default=False, verbose_name='Assembly', help_text='Can this part be built from other parts?')
+ assembly = models.BooleanField(default=False, verbose_name='Assembly', help_text=_('Can this part be built from other parts?'))
- component = models.BooleanField(default=True, verbose_name='Component', help_text='Can this part be used to build other parts?')
+ component = models.BooleanField(default=True, verbose_name='Component', help_text=_('Can this part be used to build other parts?'))
- trackable = models.BooleanField(default=False, help_text='Does this part have tracking for unique items?')
+ trackable = models.BooleanField(default=False, help_text=_('Does this part have tracking for unique items?'))
- purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?')
+ purchaseable = models.BooleanField(default=True, help_text=_('Can this part be purchased from external suppliers?'))
- salable = models.BooleanField(default=False, help_text="Can this part be sold to customers?")
+ salable = models.BooleanField(default=False, help_text=_("Can this part be sold to customers?"))
- active = models.BooleanField(default=True, help_text='Is this part active?')
+ active = models.BooleanField(default=True, help_text=_('Is this part active?'))
- virtual = models.BooleanField(default=False, help_text='Is this a virtual part, such as a software product or license?')
+ virtual = models.BooleanField(default=False, help_text=_('Is this a virtual part, such as a software product or license?'))
notes = models.TextField(blank=True)
- bom_checksum = models.CharField(max_length=128, blank=True, help_text='Stored BOM checksum')
+ bom_checksum = models.CharField(max_length=128, blank=True, help_text=_('Stored BOM checksum'))
bom_checked_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
related_name='boms_checked')
@@ -517,7 +517,7 @@ class Part(models.Model):
# Calculate the minimum number of parts that can be built using each sub-part
for item in self.bom_items.all().prefetch_related('sub_part__stock_items'):
stock = item.sub_part.available_stock
- n = int(1.0 * stock / item.quantity)
+ n = int(stock / item.quantity)
if total is None or n < total:
total = n
@@ -932,9 +932,9 @@ class PartAttachment(models.Model):
related_name='attachments')
attachment = models.FileField(upload_to=attach_file,
- help_text='Select file to attach')
+ help_text=_('Select file to attach'))
- comment = models.CharField(max_length=100, help_text='File comment')
+ comment = models.CharField(max_length=100, help_text=_('File comment'))
@property
def basename(self):
@@ -994,9 +994,9 @@ class PartParameterTemplate(models.Model):
except PartParameterTemplate.DoesNotExist:
pass
- name = models.CharField(max_length=100, help_text='Parameter Name', unique=True)
+ name = models.CharField(max_length=100, help_text=_('Parameter Name'), unique=True)
- units = models.CharField(max_length=25, help_text='Parameter Units', blank=True)
+ units = models.CharField(max_length=25, help_text=_('Parameter Units'), blank=True)
class PartParameter(models.Model):
@@ -1022,11 +1022,11 @@ class PartParameter(models.Model):
# Prevent multiple instances of a parameter for a single part
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', 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', help_text=_('Parameter Template'))
- data = models.CharField(max_length=500, help_text='Parameter Value')
+ data = models.CharField(max_length=500, help_text=_('Parameter Value'))
class BomItem(models.Model):
@@ -1050,7 +1050,7 @@ class BomItem(models.Model):
# A link to the parent part
# Each part will get a reverse lookup field 'bom_items'
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items',
- help_text='Select parent part',
+ help_text=_('Select parent part'),
limit_choices_to={
'assembly': True,
})
@@ -1058,24 +1058,24 @@ class BomItem(models.Model):
# A link to the child item (sub-part)
# Each part will get a reverse lookup field 'used_in'
sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in',
- help_text='Select part to be used in BOM',
+ help_text=_('Select part to be used in BOM'),
limit_choices_to={
'component': True,
})
# Quantity required
- quantity = models.PositiveIntegerField(default=1, 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)], help_text=_('BOM quantity for this BOM item'))
overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_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, help_text=_('BOM item reference'))
# 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, 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, help_text=_('BOM line checksum'))
def get_item_hash(self):
""" Calculate the checksum hash of this BOM line item:
@@ -1161,7 +1161,7 @@ class BomItem(models.Model):
return "{n} x {child} to make {parent}".format(
parent=self.part.full_name,
child=self.sub_part.full_name,
- n=self.quantity)
+ n=helpers.decimal2string(self.quantity))
def get_overage_quantity(self, quantity):
""" Calculate overage quantity
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index 47c530fb84..12db3e3e9b 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -1,6 +1,7 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
+
{% block details %}
{% include 'part/tabs.html' with tab='detail' %}
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index dfb9b2decf..8b11f0ff30 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -1,12 +1,14 @@
{% extends "part/part_app_base.html" %}
{% load static %}
+{% load i18n %}
+{% load inventree_extras %}
{% block content %}
{% if part.active == False %}
Notes
+ {% trans "Notes" %}
{{ order.notes }}
- This part is not active:
+ {% trans "This part is not active" %}"
{% endif %}
{% if part.is_template %}
@@ -42,13 +44,13 @@
- |
---|