Merge branch 'decimal-quantity'

This commit is contained in:
Oliver Walters 2019-11-19 21:47:22 +11:00
commit b29e1ded64
31 changed files with 2177 additions and 559 deletions

View File

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

View File

@ -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 += "<small> (+" + row.overage + ") </small>";
}
return text;
},
footerFormatter: function(data) {
var quantity = 0;
data.forEach(function(item) {
quantity += item.quantity;
});
return quantity;
},
});
if (!options.editable) {

View File

@ -183,6 +183,11 @@ function loadPartTable(table, url, options={}) {
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
if (row.units) {
value += ' <i><small>' + row.units + '</small></i>';
}
return renderLink(value, '/part/' + row.pk + '/stock/');
}
else {

View File

@ -80,6 +80,8 @@ function loadStockTable(table, options) {
items += 1;
});
stock = +stock.toFixed(5);
return stock + " (" + items + " items)";
} else if (field == 'batch') {
var batches = [];

View File

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

View File

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

View File

@ -1,12 +1,14 @@
{% load i18n %}
{% load inventree_extras %}
<div class='row'>
<h4>Allocate Stock to Build</h4>
<h4>{% trans "Allocate Stock to Build" %}</h4>
<div class='col-sm-6'>
</div>
<div class='col-sm-6'>
<div class='btn-group' style='float: right;'>
<button class='btn btn-primary' type='button' title='Automatic allocation' id='auto-allocate-build'>Auto Allocate</button>
<button class='btn btn-warning' type='button' title='Unallocate build stock' id='unallocate-build'>Unallocate</button>
<button class='btn btn-primary' type='button' title='Automatic allocation' id='auto-allocate-build'>{% trans "Auto Allocate" %}</button>
<button class='btn btn-warning' type='button' title='Unallocate build stock' id='unallocate-build'>{% trans "Unallocate" %}</button>
</div>
</div>
</div>
@ -14,16 +16,16 @@
<div class='row'>
<div class='col-sm-6'>
<h4>Part</h4>
<h4>{% trans "Part" %}</h4>
</div>
<div class='col-sm-2'>
<h4>Available</h4>
<h4>{% trans "Available" %}</h4>
</div>
<div class='col-sm-2'>
<h4>Required</h4>
<h4>{% trans "Required" %}</h4>
</div>
<div class='col-sm-2'>
<h4>Allocated</h4>
<h4>{% trans "Allocated" %}</h4>
</div>
</div>

View File

@ -1,22 +1,25 @@
<h4>Required Parts</h4>
{% load i18n %}
{% load inventree_extras %}
<h4>{% trans "Required Parts" %}</h4>
<hr>
<div id='build-item-toolbar'>
<div class='btn-group'>
<button class='btn btn-primary' type='button' id='btn-allocate' title='Allocate Stock'>Allocate</button>
<button class='btn btn-primary' type='button' id='btn-order-parts' title='Order Parts'>Order Parts</button>
<button class='btn btn-primary' type='button' id='btn-allocate' title='Allocate Stock'>{% trans "Allocate" %}</button>
<button class='btn btn-primary' type='button' id='btn-order-parts' title='Order Parts'>{% trans "Order Parts" %}</button>
</div>
</div>
<table class='table table-striped table-condensed' id='build-list' data-sorting='true' data-toolbar='#build-item-toolbar'>
<thead>
<tr>
<th data-sortable='true'>Part</th>
<th>Description</th>
<th data-sortable='true'>Available</th>
<th data-sortable='true'>Required</th>
<th data-sortable='true'>Allocated</th>
<th data-sortable='true'>On Order</th>
<th data-sortable='true'>{% trans "Part" %}</th>
<th>{% trans "Description" %}</th>
<th data-sortable='true'>{% trans "Available" %}</th>
<th data-sortable='true'>{% trans "Required" %}</th>
<th data-sortable='true'>{% trans "Allocated" %}</th>
<th data-sortable='true'>{% trans "On Order" %}</th>
</tr>
</thead>
<tbody>
@ -27,10 +30,10 @@
<a class='hover-icon'a href="{% url 'part-detail' item.part.id %}">{{ item.part.full_name }}</a>
</td>
<td>{{ item.part.description }}</td>
<td>{{ item.part.total_stock }}</td>
<td>{{ item.quantity }}</td>
<td>{% decimal item.part.total_stock %}</td>
<td>{% decimal item.quantity %}</td>
<td>{{ item.allocated }}</td>
<td>{{ item.part.on_order }}</td>
<td>{% decimal item.part.on_order %}</td>
</tr>
{% endfor %}
</tbody>

View File

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

View File

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

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

@ -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 = [
('order', '0012_auto_20190617_1943'),
]
operations = [
migrations.AlterField(
model_name='purchaseorderlineitem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.5 on 2019-11-18 23:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0013_auto_20191118_2323'),
]
operations = [
migrations.AlterField(
model_name='purchaseorderlineitem',
name='received',
field=models.DecimalField(decimal_places=5, default=0, help_text='Number of items received', max_digits=15),
),
]

View File

@ -17,6 +17,7 @@ from datetime import datetime
from stock.models import StockItem
from company.models import Company, SupplierPart
from InvenTree.helpers import decimal2string
from InvenTree.status_codes import OrderStatus
@ -242,7 +243,7 @@ class OrderLineItem(models.Model):
class Meta:
abstract = True
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1, help_text=_('Item quantity'))
quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, help_text=_('Item quantity'))
reference = models.CharField(max_length=100, blank=True, help_text=_('Line item reference'))
@ -264,7 +265,7 @@ class PurchaseOrderLineItem(OrderLineItem):
def __str__(self):
return "{n} x {part} from {supplier} (for {po})".format(
n=self.quantity,
n=decimal2string(self.quantity),
part=self.part.SKU if self.part else 'unknown part',
supplier=self.order.supplier.name,
po=self.order)
@ -284,7 +285,7 @@ class PurchaseOrderLineItem(OrderLineItem):
help_text=_("Supplier part"),
)
received = models.PositiveIntegerField(default=0, help_text=_('Number of items received'))
received = models.DecimalField(decimal_places=5, max_digits=15, default=0, help_text=_('Number of items received'))
def remaining(self):
""" Calculate the number of items remaining to be received """

View File

@ -1,6 +1,8 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load inventree_extras %}
{% block page_title %}
InvenTree | {{ order }}
@ -55,29 +57,29 @@ InvenTree | {{ order }}
</div>
</div>
<div class='col-sm-6'>
<h4>Purchase Order Details</h4>
<h4>{% trans "Purchase Order Details" %}</h4>
<table class='table'>
<tr>
<td>Supplier</td>
<td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail' order.supplier.id %}">{{ order.supplier }}</a></td>
</tr>
<tr>
<td>Status</td>
<td>{% trans "Status" %}</td>
<td>{% include "order/order_status.html" %}</td>
</tr>
<tr>
<td>Created</td>
<td>{% trans "Created" %}</td>
<td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
</tr>
{% if order.issue_date %}
<tr>
<td>Issued</td>
<td>{% trans "Issued" %}</td>
<td>{{ order.issue_date }}</td>
</tr>
{% endif %}
{% if order.status == OrderStatus.COMPLETE %}
<tr>
<td>Received</td>
<td>{% trans "Received" %}</td>
<td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td>
</tr>
{% endif %}
@ -98,16 +100,16 @@ InvenTree | {{ order }}
<table class='table table-striped table-condensed' id='po-lines-table' data-toolbar='#order-toolbar-buttons'>
<thead>
<tr>
<th data-sortable='true'>Line</th>
<th data-sortable='true'>Part</th>
<th>Description</th>
<th data-sortable='true'>Order Code</th>
<th data-sortable='true'>Reference</th>
<th data-sortable='true'>Quantity</th>
<th data-sortable='true'>{% trans "Line" %}</th>
<th data-sortable='true'>{% trans "Part" %}</th>
<th>{% trans "Description" %}</th>
<th data-sortable='true'>{% trans "Order Code" %}</th>
<th data-sortable='true'>{% trans "Reference" %}</th>
<th data-sortable='true'>{% trans "Quantity" %}</th>
{% if not order.status == OrderStatus.PENDING %}
<th data-sortable='true'>Received</th>
<th data-sortable='true'>{% trans "Received" %}</th>
{% endif %}
<th>Note</th>
<th>{% trans "Note" %}</th>
<th></th>
</tr>
</thead>
@ -128,9 +130,9 @@ InvenTree | {{ order }}
<td colspan='3'><strong>Warning: Part has been deleted.</strong></td>
{% endif %}
<td>{{ line.reference }}</td>
<td>{{ line.quantity }}</td>
<td>{% decimal line.quantity %}</td>
{% if not order.status == OrderStatus.PENDING %}
<td>{{ line.received }}</td>
<td>{% decimal line.received %}</td>
{% endif %}
<td>
{{ line.notes }}
@ -160,7 +162,7 @@ InvenTree | {{ order }}
{% if order.notes %}
<hr>
<div class='panel panel-default'>
<div class='panel-heading'><b>Notes</b></div>
<div class='panel-heading'><b>{% trans "Notes" %}</b></div>
<div class='panel-body'>{{ order.notes }}</div>
</div>
{% endif %}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include 'part/tabs.html' with tab='detail' %}

View File

@ -1,12 +1,14 @@
{% extends "part/part_app_base.html" %}
{% load static %}
{% load i18n %}
{% load inventree_extras %}
{% block content %}
{% if part.active == False %}
<div class='alert alert-danger alert-block'>
This part is not active:
{% trans "This part is not active" %}"
</div>
{% endif %}
{% if part.is_template %}
@ -42,13 +44,13 @@
<p>
<div class='btn-row'>
<div class='btn-group'>
<button type='button' class='btn btn-default btn-glyph' id='toggle-starred' title='Star this part'>
<button type='button' class='btn btn-default btn-glyph' id='toggle-starred' title='{% trans "Star this part" %}'>
<span id='part-star-icon' class='starred-part glyphicon {% if starred %}glyphicon-star{% else %}glyphicon-star-empty{% endif %}'/>
</button>
{% if part.is_template == False %}
{% include "qr_button.html" %}
{% if part.active %}
<button type='button' class='btn btn-default btn-glyph' id='price-button' title='Show pricing information'>
<button type='button' class='btn btn-default btn-glyph' id='price-button' title='{% trans "Show pricing information" %}'>
<span id='part-price-icon' class='part-price glyphicon glyphicon-usd'/>
</button>
{% if not part.virtual %}
@ -80,13 +82,13 @@
<table class='table table-condensed'>
{% if part.IPN %}
<tr>
<td>IPN</td>
<td>{% trans "IPN" %}</td>
<td>{{ part.IPN }}</td>
</tr>
{% endif %}
{% if part.URL %}
<tr>
<td>URL</td>
<td>{% trans "URL" %}</td>
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr>
{% endif %}
@ -100,25 +102,25 @@
<table class="table table-striped">
<tr>
<td>
<h4>Available Stock</h4>
<h4>{% trans "Available Stock" %}</h4>
</td>
<td><h4>{{ part.available_stock }} {{ part.units }}</h4></td>
<td><h4>{% decimal part.available_stock %} {{ part.units }}</h4></td>
</tr>
<tr>
<td>In Stock</td>
<td>{{ part.total_stock }}</td>
<td>{% trans "In Stock" %}</td>
<td>{% decimal part.total_stock %}</td>
</tr>
{% if not part.is_template %}
{% if part.allocation_count > 0 %}
<tr>
<td>Allocated</td>
<td>{{ part.allocation_count }}</td>
<td>{% trans "Allocated" %}</td>
<td>{% decimal part.allocation_count %}</td>
</tr>
{% endif %}
{% if part.on_order > 0 %}
<tr>
<td>On Order</td>
<td>{{ part.on_order }}</td>
<td>{% trans "On Order" %}</td>
<td>{% decimal part.on_order %}</td>
</tr>
{% endif %}
{% endif %}
@ -126,17 +128,17 @@
{% if part.assembly %}
<tr>
<td colspan='2'>
<b>Build Status</b>
<b>{% trans "Build Status" %}</b>
</td>
</tr>
<tr>
<td>Can Build</td>
<td>{{ part.can_build }}</td>
<td>{% trans "Can Build" %}</td>
<td>{% decimal part.can_build %}</td>
</tr>
{% if part.quantity_being_built > 0 %}
<tr>
<td>Underway</td>
<td>{{ part.quantity_being_built }}</td>
<td>{% trans "Underway" %}</td>
<td>{% decimal part.quantity_being_built %}</td>
</tr>
{% endif %}
{% endif %}

View File

@ -1,54 +1,57 @@
{% load i18n %}
{% load inventree_extras %}
<ul class="nav nav-tabs">
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}>
<a href="{% url 'part-detail' part.id %}">Details</a>
<a href="{% url 'part-detail' part.id %}">{% trans "Details" %}</a>
</li>
<li{% ifequal tab 'params' %} class='active'{% endifequal %}>
<a href="{% url 'part-params' part.id %}">Parameters <span class='badge'>{{ part.parameters.count }}</span></a>
<a href="{% url 'part-params' part.id %}">{% trans "Parameters" %} <span class='badge'>{{ part.parameters.count }}</span></a>
</li>
{% if part.is_template %}
<li{% ifequal tab 'variants' %} class='active'{% endifequal %}>
<a href="{% url 'part-variants' part.id %}">Variants <span class='badge'>{{ part.variants.count }}</span></span></a>
<a href="{% url 'part-variants' part.id %}">{% trans "Variants" %} <span class='badge'>{{ part.variants.count }}</span></span></a>
</li>
{% endif %}
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
<a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.total_stock }}</span></a>
<a href="{% url 'part-stock' part.id %}">{% trans "Stock" %} <span class="badge">{% decimal part.total_stock %}</span></a>
</li>
{% if part.component or part.used_in_count > 0 %}
<li{% ifequal tab 'allocation' %} class="active"{% endifequal %}>
<a href="{% url 'part-allocation' part.id %}">Allocated <span class="badge">{{ part.allocation_count }}</span></a>
<a href="{% url 'part-allocation' part.id %}">{% trans "Allocated" %} <span class="badge">{% decimal part.allocation_count %}</span></a>
</li>
{% endif %}
{% if part.assembly %}
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}>
<a href="{% url 'part-bom' part.id %}">BOM<span class="badge{% if part.is_bom_valid == False %} badge-alert{% endif %}">{{ part.bom_count }}</span></a></li>
<a href="{% url 'part-bom' part.id %}">{% trans "BOM" %}<span class="badge{% if part.is_bom_valid == False %} badge-alert{% endif %}">{{ part.bom_count }}</span></a></li>
<li{% ifequal tab 'build' %} class="active"{% endifequal %}>
<a href="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.active_builds|length }}</span></a></li>
<a href="{% url 'part-build' part.id %}">{% trans "Build" %}<span class='badge'>{{ part.active_builds|length }}</span></a></li>
{% endif %}
{% if part.component or part.used_in_count > 0 %}
<li{% ifequal tab 'used' %} class="active"{% endifequal %}>
<a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
<a href="{% url 'part-used-in' part.id %}">{% trans "Used In" %} {% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
{% endif %}
{% if part.purchaseable %}
{% if part.is_template == False %}
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
<a href="{% url 'part-suppliers' part.id %}">Suppliers
<a href="{% url 'part-suppliers' part.id %}">{% trans "Suppliers" %}
<span class="badge">{{ part.supplier_count }}</span>
</a>
</li>
{% endif %}
<li{% ifequal tab 'orders' %} class='active'{% endifequal %}>
<a href="{% url 'part-orders' part.id %}">Purchase Orders <span class='badge'>{{ part.purchase_orders|length }}</span></a>
<a href="{% url 'part-orders' part.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ part.purchase_orders|length }}</span></a>
</li>
{% endif %}
{% if part.trackable and 0 %}
<li{% ifequal tab 'track' %} class="active"{% endifequal %}>
<a href="{% url 'part-track' part.id %}">Tracking
<a href="{% url 'part-track' part.id %}">{% trans "Tracking" %}
{% if parts.serials.all|length > 0 %}
<span class="badge">{{ part.serials.all|length }}</span>
{% endif %}
</a></li>
{% endif %}
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
<a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a>
<a href="{% url 'part-attachments' part.id %}">{% trans "Attachments" %} {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a>
</li>
</ul>

View File

@ -4,10 +4,18 @@ over and above the built-in Django tags.
from django import template
from InvenTree import version
from InvenTree.helpers import decimal2string
register = template.Library()
@register.simple_tag()
def decimal(x, *args, **kwargs):
""" Simplified rendering of a decimal number """
return decimal2string(x)
@register.simple_tag()
def inrange(n, *args, **kwargs):
""" Return range(n) for iterating through a numeric quantity """

View File

@ -123,7 +123,7 @@ class PartAPITest(APITestCase):
url = reverse('api-bom-item-detail', kwargs={'pk': 3})
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['quantity'], 25)
self.assertEqual(int(float(response.data['quantity'])), 25)
# Increase the quantity
data = response.data
@ -134,7 +134,7 @@ class PartAPITest(APITestCase):
# Check that the quantity was increased and a note added
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['quantity'], 57)
self.assertEqual(int(float(response.data['quantity'])), 57)
self.assertEqual(response.data['note'], 'Added a note')
def test_add_bom_item(self):

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.5 on 2019-11-18 21:46
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0015_auto_20190913_1407'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.5 on 2019-11-18 23:11
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0016_auto_20191118_2146'),
]
operations = [
migrations.AlterField(
model_name='stockitemtracking',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@ -18,6 +18,7 @@ from django.dispatch import receiver
from mptt.models import TreeForeignKey
from decimal import Decimal, InvalidOperation
from datetime import datetime
from InvenTree import helpers
@ -294,43 +295,43 @@ class StockItem(models.Model):
)
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='stock_items', help_text='Base part',
related_name='stock_items', help_text=_('Base part'),
limit_choices_to={
'is_template': False,
'active': True,
})
supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
help_text='Select a matching supplier part for this stock item')
help_text=_('Select a matching supplier part for this stock item'))
location = TreeForeignKey(StockLocation, on_delete=models.DO_NOTHING,
related_name='stock_items', blank=True, null=True,
help_text='Where is this stock item located?')
help_text=_('Where is this stock item located?'))
belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING,
related_name='owned_parts', blank=True, null=True,
help_text='Is this item installed in another item?')
help_text=_('Is this item installed in another item?'))
customer = models.ForeignKey('company.Company', on_delete=models.SET_NULL,
related_name='stockitems', blank=True, null=True,
help_text='Item assigned to customer?')
help_text=_('Item assigned to customer?'))
serial = models.PositiveIntegerField(blank=True, null=True,
help_text='Serial number for this item')
help_text=_('Serial number for this item'))
URL = InvenTreeURLField(max_length=125, blank=True)
batch = models.CharField(max_length=100, blank=True, null=True,
help_text='Batch code for this stock item')
help_text=_('Batch code for this stock item'))
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)
updated = models.DateField(auto_now=True, null=True)
build = models.ForeignKey(
'build.Build', on_delete=models.SET_NULL,
blank=True, null=True,
help_text='Build for this stock item',
help_text=_('Build for this stock item'),
related_name='build_outputs',
)
@ -339,7 +340,7 @@ class StockItem(models.Model):
on_delete=models.SET_NULL,
related_name='stock_items',
blank=True, null=True,
help_text='Purchase order for this stock item'
help_text=_('Purchase order for this stock item')
)
# last time the stock was checked / counted
@ -350,7 +351,7 @@ class StockItem(models.Model):
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, help_text=_('Delete this Stock Item when stock is depleted'))
status = models.PositiveIntegerField(
default=StockStatus.OK,
@ -510,6 +511,11 @@ class StockItem(models.Model):
if self.serialized:
return
try:
quantity = Decimal(quantity)
except (InvalidOperation, ValueError):
return
# Doesn't make sense for a zero quantity
if quantity <= 0:
return
@ -549,7 +555,10 @@ class StockItem(models.Model):
quantity: If provided, override the quantity (default = total stock quantity)
"""
quantity = int(kwargs.get('quantity', self.quantity))
try:
quantity = Decimal(kwargs.get('quantity', self.quantity))
except InvalidOperation:
return False
if quantity <= 0:
return False
@ -599,12 +608,19 @@ class StockItem(models.Model):
if self.serialized:
return
try:
self.quantity = Decimal(quantity)
except (InvalidOperation, ValueError):
return
if quantity < 0:
quantity = 0
self.quantity = quantity
if quantity <= 0 and self.delete_on_deplete and self.can_delete():
if quantity == 0 and self.delete_on_deplete and self.can_delete():
# TODO - Do not actually "delete" stock at this point - instead give it a "DELETED" flag
self.delete()
return False
else:
@ -618,7 +634,10 @@ class StockItem(models.Model):
record the date of stocktake
"""
count = int(count)
try:
count = Decimal(count)
except InvalidOperation:
return False
if count < 0 or self.infinite:
return False
@ -646,7 +665,10 @@ class StockItem(models.Model):
if self.serialized:
return False
quantity = int(quantity)
try:
quantity = Decimal(quantity)
except InvalidOperation:
return False
# Ignore amounts that do not make sense
if quantity <= 0 or self.infinite:
@ -670,7 +692,10 @@ class StockItem(models.Model):
if self.serialized:
return False
quantity = int(quantity)
try:
quantity = Decimal(quantity)
except InvalidOperation:
return False
if quantity <= 0 or self.infinite:
return False
@ -691,7 +716,7 @@ class StockItem(models.Model):
sn=self.serial)
else:
s = '{n} x {part}'.format(
n=self.quantity,
n=helpers.decimal2string(self.quantity),
part=self.part.full_name)
if self.location:
@ -722,17 +747,17 @@ class StockItemTracking(models.Model):
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, 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, help_text=_('Entry notes'))
URL = InvenTreeURLField(blank=True, help_text='Link to external page for further information')
URL = InvenTreeURLField(blank=True, help_text=_('Link to external page for further information'))
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
system = models.BooleanField(default=False)
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)
# TODO
# image = models.ImageField(upload_to=func, max_length=255, null=True, blank=True)

View File

@ -1,5 +1,6 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% load inventree_extras %}
{% load i18n %}
{% block content %}
@ -86,7 +87,7 @@
{% else %}
<tr>
<td>{% trans "Quantity" %}</td>
<td>{{ item.quantity }}</td>
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
</tr>
{% endif %}
{% if item.batch %}

View File

@ -245,7 +245,7 @@ class StockTest(TestCase):
w1 = StockItem.objects.get(pk=100)
w2 = StockItem.objects.get(pk=101)
# Take 25 units from w1
# Take 25 units from w1 (there are only 10 in stock)
w1.take_stock(30, None, notes='Took 30')
# Get from database again

View File

@ -20,6 +20,8 @@ from InvenTree.views import QRCodeView
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
from InvenTree.helpers import ExtractSerialNumbers
from decimal import Decimal, InvalidOperation
from datetime import datetime
from company.models import Company
@ -91,7 +93,7 @@ class StockLocationEdit(AjaxUpdateView):
form_class = EditStockLocationForm
context_object_name = 'location'
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Stock Location'
ajax_form_title = _('Edit Stock Location')
def get_form(self):
""" Customize form data for StockLocation editing.
@ -115,7 +117,7 @@ class StockLocationEdit(AjaxUpdateView):
class StockLocationQRCode(QRCodeView):
""" View for displaying a QR code for a StockLocation object """
ajax_form_title = "Stock Location QR code"
ajax_form_title = _("Stock Location QR code")
def get_qr_data(self):
""" Generate QR code data for the StockLocation """
@ -130,7 +132,7 @@ class StockExportOptions(AjaxView):
""" Form for selecting StockExport options """
model = StockLocation
ajax_form_title = 'Stock Export Options'
ajax_form_title = _('Stock Export Options')
form_class = ExportOptionsForm
def post(self, request, *args, **kwargs):
@ -238,7 +240,7 @@ class StockExport(AjaxView):
class StockItemQRCode(QRCodeView):
""" View for displaying a QR code for a StockItem object """
ajax_form_title = "Stock Item QR Code"
ajax_form_title = _("Stock Item QR Code")
def get_qr_data(self):
""" Generate QR code data for the StockItem """
@ -261,7 +263,7 @@ class StockAdjust(AjaxView, FormMixin):
"""
ajax_template_name = 'stock/stock_adjust.html'
ajax_form_title = 'Adjust Stock'
ajax_form_title = _('Adjust Stock')
form_class = AdjustStockForm
stock_items = []
@ -398,8 +400,9 @@ class StockAdjust(AjaxView, FormMixin):
valid = form.is_valid()
for item in self.stock_items:
try:
item.new_quantity = int(item.new_quantity)
item.new_quantity = Decimal(item.new_quantity)
except ValueError:
item.error = _('Must enter integer value')
valid = False
@ -543,7 +546,7 @@ class StockAdjust(AjaxView, FormMixin):
if destination == item.location and item.new_quantity == item.quantity:
continue
item.move(destination, note, self.request.user, quantity=int(item.new_quantity))
item.move(destination, note, self.request.user, quantity=item.new_quantity)
count += 1
@ -582,7 +585,7 @@ class StockItemEdit(AjaxUpdateView):
form_class = EditStockItemForm
context_object_name = 'item'
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Stock Item'
ajax_form_title = _('Edit Stock Item')
def get_form(self):
""" Get form for StockItem editing.
@ -618,7 +621,7 @@ class StockLocationCreate(AjaxCreateView):
form_class = EditStockLocationForm
context_object_name = 'location'
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Create new Stock Location'
ajax_form_title = _('Create new Stock Location')
def get_initial(self):
initials = super(StockLocationCreate, self).get_initial().copy()
@ -639,7 +642,7 @@ class StockItemSerialize(AjaxUpdateView):
model = StockItem
ajax_template_name = 'stock/item_serialize.html'
ajax_form_title = 'Serialize Stock'
ajax_form_title = _('Serialize Stock')
form_class = SerializeStockForm
def get_initial(self):
@ -719,7 +722,7 @@ class StockItemCreate(AjaxCreateView):
form_class = CreateStockItemForm
context_object_name = 'item'
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Create new Stock Item'
ajax_form_title = _('Create new Stock Item')
def get_form(self):
""" Get form for StockItem creation.
@ -783,7 +786,7 @@ class StockItemCreate(AjaxCreateView):
try:
original = StockItem.objects.get(pk=item_to_copy)
initials = model_to_dict(original)
self.ajax_form_title = "Copy Stock Item"
self.ajax_form_title = _("Copy Stock Item")
except StockItem.DoesNotExist:
initials = super(StockItemCreate, self).get_initial().copy()
@ -828,11 +831,12 @@ class StockItemCreate(AjaxCreateView):
part_id = form['part'].value()
try:
part = Part.objects.get(id=part_id)
quantity = int(form['quantity'].value())
except (Part.DoesNotExist, ValueError):
quantity = Decimal(form['quantity'].value())
except (Part.DoesNotExist, ValueError, InvalidOperation):
part = None
quantity = 1
valid = False
form.errors['quantity'] = [_('Invalid quantity')]
if part is None:
form.errors['part'] = [_('Invalid part selection')]
@ -914,7 +918,7 @@ class StockLocationDelete(AjaxDeleteView):
success_url = '/stock'
ajax_template_name = 'stock/location_delete.html'
context_object_name = 'location'
ajax_form_title = 'Delete Stock Location'
ajax_form_title = _('Delete Stock Location')
class StockItemDelete(AjaxDeleteView):
@ -927,7 +931,7 @@ class StockItemDelete(AjaxDeleteView):
success_url = '/stock/'
ajax_template_name = 'stock/item_delete.html'
context_object_name = 'item'
ajax_form_title = 'Delete Stock Item'
ajax_form_title = _('Delete Stock Item')
class StockItemTrackingDelete(AjaxDeleteView):
@ -938,7 +942,7 @@ class StockItemTrackingDelete(AjaxDeleteView):
model = StockItemTracking
ajax_template_name = 'stock/tracking_delete.html'
ajax_form_title = 'Delete Stock Tracking Entry'
ajax_form_title = _('Delete Stock Tracking Entry')
class StockTrackingIndex(ListView):
@ -955,7 +959,7 @@ class StockItemTrackingEdit(AjaxUpdateView):
""" View for editing a StockItemTracking object """
model = StockItemTracking
ajax_form_title = 'Edit Stock Tracking Entry'
ajax_form_title = _('Edit Stock Tracking Entry')
form_class = TrackingEntryForm
@ -964,7 +968,7 @@ class StockItemTrackingCreate(AjaxCreateView):
"""
model = StockItemTracking
ajax_form_title = "Add Stock Tracking Entry"
ajax_form_title = _("Add Stock Tracking Entry")
form_class = TrackingEntryForm
def post(self, request, *args, **kwargs):