mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'decimal-quantity'
This commit is contained in:
commit
b29e1ded64
@ -52,6 +52,24 @@ def str2bool(text, test=True):
|
|||||||
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ]
|
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='"'):
|
def WrapWithQuotes(text, quote='"'):
|
||||||
""" Wrap the supplied text with quotes
|
""" Wrap the supplied text with quotes
|
||||||
|
|
||||||
|
@ -163,21 +163,16 @@ function loadBomTable(table, options) {
|
|||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
var text = value;
|
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) {
|
if (row.overage) {
|
||||||
text += "<small> (+" + row.overage + ") </small>";
|
text += "<small> (+" + row.overage + ") </small>";
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
},
|
},
|
||||||
footerFormatter: function(data) {
|
|
||||||
var quantity = 0;
|
|
||||||
|
|
||||||
data.forEach(function(item) {
|
|
||||||
quantity += item.quantity;
|
|
||||||
});
|
|
||||||
|
|
||||||
return quantity;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!options.editable) {
|
if (!options.editable) {
|
||||||
|
@ -183,6 +183,11 @@ function loadPartTable(table, url, options={}) {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|
||||||
|
if (row.units) {
|
||||||
|
value += ' <i><small>' + row.units + '</small></i>';
|
||||||
|
}
|
||||||
|
|
||||||
return renderLink(value, '/part/' + row.pk + '/stock/');
|
return renderLink(value, '/part/' + row.pk + '/stock/');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -80,6 +80,8 @@ function loadStockTable(table, options) {
|
|||||||
items += 1;
|
items += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stock = +stock.toFixed(5);
|
||||||
|
|
||||||
return stock + " (" + items + " items)";
|
return stock + " (" + items + " items)";
|
||||||
} else if (field == 'batch') {
|
} else if (field == 'batch') {
|
||||||
var batches = [];
|
var batches = [];
|
||||||
|
19
InvenTree/build/migrations/0007_auto_20191118_2321.py
Normal file
19
InvenTree/build/migrations/0007_auto_20191118_2321.py
Normal 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)]),
|
||||||
|
),
|
||||||
|
]
|
@ -48,7 +48,7 @@ class Build(models.Model):
|
|||||||
title = models.CharField(
|
title = models.CharField(
|
||||||
blank=False,
|
blank=False,
|
||||||
max_length=100,
|
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,
|
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||||
related_name='builds',
|
related_name='builds',
|
||||||
@ -57,28 +57,28 @@ class Build(models.Model):
|
|||||||
'assembly': True,
|
'assembly': True,
|
||||||
'active': 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,
|
take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
|
||||||
related_name='sourcing_builds',
|
related_name='sourcing_builds',
|
||||||
null=True, blank=True,
|
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(
|
quantity = models.PositiveIntegerField(
|
||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
help_text='Number of parts to build'
|
help_text=_('Number of parts to build')
|
||||||
)
|
)
|
||||||
|
|
||||||
status = models.PositiveIntegerField(default=BuildStatus.PENDING,
|
status = models.PositiveIntegerField(default=BuildStatus.PENDING,
|
||||||
choices=BuildStatus.items(),
|
choices=BuildStatus.items(),
|
||||||
validators=[MinValueValidator(0)],
|
validators=[MinValueValidator(0)],
|
||||||
help_text='Build status')
|
help_text=_('Build status'))
|
||||||
|
|
||||||
batch = models.CharField(max_length=100, blank=True, null=True,
|
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)
|
creation_date = models.DateField(auto_now=True, editable=False)
|
||||||
|
|
||||||
@ -90,10 +90,9 @@ class Build(models.Model):
|
|||||||
related_name='builds_completed'
|
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 = models.TextField(blank=True, help_text=_('Extra build notes'))
|
||||||
""" Notes attached to each build output """
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def cancelBuild(self, user):
|
def cancelBuild(self, user):
|
||||||
@ -399,18 +398,20 @@ class BuildItem(models.Model):
|
|||||||
Build,
|
Build,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='allocated_stock',
|
related_name='allocated_stock',
|
||||||
help_text='Build to allocate parts'
|
help_text=_('Build to allocate parts')
|
||||||
)
|
)
|
||||||
|
|
||||||
stock_item = models.ForeignKey(
|
stock_item = models.ForeignKey(
|
||||||
'stock.StockItem',
|
'stock.StockItem',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='allocations',
|
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,
|
default=1,
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
help_text='Stock quantity to allocate to build'
|
help_text=_('Stock quantity to allocate to build')
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<h4>Allocate Stock to Build</h4>
|
<h4>{% trans "Allocate Stock to Build" %}</h4>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<div class='btn-group' style='float: right;'>
|
<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-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'>Unallocate</button>
|
<button class='btn btn-warning' type='button' title='Unallocate build stock' id='unallocate-build'>{% trans "Unallocate" %}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -14,16 +16,16 @@
|
|||||||
|
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<h4>Part</h4>
|
<h4>{% trans "Part" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-2'>
|
<div class='col-sm-2'>
|
||||||
<h4>Available</h4>
|
<h4>{% trans "Available" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-2'>
|
<div class='col-sm-2'>
|
||||||
<h4>Required</h4>
|
<h4>{% trans "Required" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-2'>
|
<div class='col-sm-2'>
|
||||||
<h4>Allocated</h4>
|
<h4>{% trans "Allocated" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
<h4>Required Parts</h4>
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
<h4>{% trans "Required Parts" %}</h4>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div id='build-item-toolbar'>
|
<div id='build-item-toolbar'>
|
||||||
<div class='btn-group'>
|
<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-allocate' title='Allocate Stock'>{% trans "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-order-parts' title='Order Parts'>{% trans "Order Parts" %}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='build-list' data-sorting='true' data-toolbar='#build-item-toolbar'>
|
<table class='table table-striped table-condensed' id='build-list' data-sorting='true' data-toolbar='#build-item-toolbar'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable='true'>Part</th>
|
<th data-sortable='true'>{% trans "Part" %}</th>
|
||||||
<th>Description</th>
|
<th>{% trans "Description" %}</th>
|
||||||
<th data-sortable='true'>Available</th>
|
<th data-sortable='true'>{% trans "Available" %}</th>
|
||||||
<th data-sortable='true'>Required</th>
|
<th data-sortable='true'>{% trans "Required" %}</th>
|
||||||
<th data-sortable='true'>Allocated</th>
|
<th data-sortable='true'>{% trans "Allocated" %}</th>
|
||||||
<th data-sortable='true'>On Order</th>
|
<th data-sortable='true'>{% trans "On Order" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -27,10 +30,10 @@
|
|||||||
<a class='hover-icon'a href="{% url 'part-detail' item.part.id %}">{{ item.part.full_name }}</a>
|
<a class='hover-icon'a href="{% url 'part-detail' item.part.id %}">{{ item.part.full_name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.part.description }}</td>
|
<td>{{ item.part.description }}</td>
|
||||||
<td>{{ item.part.total_stock }}</td>
|
<td>{% decimal item.part.total_stock %}</td>
|
||||||
<td>{{ item.quantity }}</td>
|
<td>{% decimal item.quantity %}</td>
|
||||||
<td>{{ item.allocated }}</td>
|
<td>{{ item.allocated }}</td>
|
||||||
<td>{{ item.part.on_order }}</td>
|
<td>{% decimal item.part.on_order %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
19
InvenTree/company/migrations/0009_auto_20191118_2323.py
Normal file
19
InvenTree/company/migrations/0009_auto_20191118_2323.py
Normal 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)]),
|
||||||
|
),
|
||||||
|
]
|
@ -379,7 +379,7 @@ class SupplierPriceBreak(models.Model):
|
|||||||
|
|
||||||
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks')
|
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)])
|
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
19
InvenTree/order/migrations/0013_auto_20191118_2323.py
Normal file
19
InvenTree/order/migrations/0013_auto_20191118_2323.py
Normal 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)]),
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/order/migrations/0014_auto_20191118_2328.py
Normal file
18
InvenTree/order/migrations/0014_auto_20191118_2328.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -17,6 +17,7 @@ from datetime import datetime
|
|||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
|
|
||||||
|
from InvenTree.helpers import decimal2string
|
||||||
from InvenTree.status_codes import OrderStatus
|
from InvenTree.status_codes import OrderStatus
|
||||||
|
|
||||||
|
|
||||||
@ -242,7 +243,7 @@ class OrderLineItem(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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'))
|
reference = models.CharField(max_length=100, blank=True, help_text=_('Line item reference'))
|
||||||
|
|
||||||
@ -264,7 +265,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{n} x {part} from {supplier} (for {po})".format(
|
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',
|
part=self.part.SKU if self.part else 'unknown part',
|
||||||
supplier=self.order.supplier.name,
|
supplier=self.order.supplier.name,
|
||||||
po=self.order)
|
po=self.order)
|
||||||
@ -284,7 +285,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
help_text=_("Supplier part"),
|
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):
|
def remaining(self):
|
||||||
""" Calculate the number of items remaining to be received """
|
""" Calculate the number of items remaining to be received """
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {{ order }}
|
InvenTree | {{ order }}
|
||||||
@ -55,29 +57,29 @@ InvenTree | {{ order }}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<h4>Purchase Order Details</h4>
|
<h4>{% trans "Purchase Order Details" %}</h4>
|
||||||
<table class='table'>
|
<table class='table'>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Supplier</td>
|
<td>{% trans "Supplier" %}</td>
|
||||||
<td><a href="{% url 'company-detail' order.supplier.id %}">{{ order.supplier }}</a></td>
|
<td><a href="{% url 'company-detail' order.supplier.id %}">{{ order.supplier }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Status</td>
|
<td>{% trans "Status" %}</td>
|
||||||
<td>{% include "order/order_status.html" %}</td>
|
<td>{% include "order/order_status.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Created</td>
|
<td>{% trans "Created" %}</td>
|
||||||
<td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
|
<td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if order.issue_date %}
|
{% if order.issue_date %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Issued</td>
|
<td>{% trans "Issued" %}</td>
|
||||||
<td>{{ order.issue_date }}</td>
|
<td>{{ order.issue_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if order.status == OrderStatus.COMPLETE %}
|
{% if order.status == OrderStatus.COMPLETE %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Received</td>
|
<td>{% trans "Received" %}</td>
|
||||||
<td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td>
|
<td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -98,16 +100,16 @@ InvenTree | {{ order }}
|
|||||||
<table class='table table-striped table-condensed' id='po-lines-table' data-toolbar='#order-toolbar-buttons'>
|
<table class='table table-striped table-condensed' id='po-lines-table' data-toolbar='#order-toolbar-buttons'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable='true'>Line</th>
|
<th data-sortable='true'>{% trans "Line" %}</th>
|
||||||
<th data-sortable='true'>Part</th>
|
<th data-sortable='true'>{% trans "Part" %}</th>
|
||||||
<th>Description</th>
|
<th>{% trans "Description" %}</th>
|
||||||
<th data-sortable='true'>Order Code</th>
|
<th data-sortable='true'>{% trans "Order Code" %}</th>
|
||||||
<th data-sortable='true'>Reference</th>
|
<th data-sortable='true'>{% trans "Reference" %}</th>
|
||||||
<th data-sortable='true'>Quantity</th>
|
<th data-sortable='true'>{% trans "Quantity" %}</th>
|
||||||
{% if not order.status == OrderStatus.PENDING %}
|
{% if not order.status == OrderStatus.PENDING %}
|
||||||
<th data-sortable='true'>Received</th>
|
<th data-sortable='true'>{% trans "Received" %}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>Note</th>
|
<th>{% trans "Note" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -128,9 +130,9 @@ InvenTree | {{ order }}
|
|||||||
<td colspan='3'><strong>Warning: Part has been deleted.</strong></td>
|
<td colspan='3'><strong>Warning: Part has been deleted.</strong></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ line.reference }}</td>
|
<td>{{ line.reference }}</td>
|
||||||
<td>{{ line.quantity }}</td>
|
<td>{% decimal line.quantity %}</td>
|
||||||
{% if not order.status == OrderStatus.PENDING %}
|
{% if not order.status == OrderStatus.PENDING %}
|
||||||
<td>{{ line.received }}</td>
|
<td>{% decimal line.received %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{{ line.notes }}
|
{{ line.notes }}
|
||||||
@ -160,7 +162,7 @@ InvenTree | {{ order }}
|
|||||||
{% if order.notes %}
|
{% if order.notes %}
|
||||||
<hr>
|
<hr>
|
||||||
<div class='panel panel-default'>
|
<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 class='panel-body'>{{ order.notes }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
19
InvenTree/part/migrations/0024_auto_20191118_2139.py
Normal file
19
InvenTree/part/migrations/0024_auto_20191118_2139.py
Normal 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)]),
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/part/migrations/0025_auto_20191118_2316.py
Normal file
18
InvenTree/part/migrations/0025_auto_20191118_2316.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -53,10 +53,10 @@ 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,
|
||||||
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):
|
def get_absolute_url(self):
|
||||||
return reverse('category-detail', kwargs={'pk': self.id})
|
return reverse('category-detail', kwargs={'pk': self.id})
|
||||||
@ -324,11 +324,11 @@ class Part(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
name = models.CharField(max_length=100, blank=False,
|
name = models.CharField(max_length=100, blank=False,
|
||||||
help_text='Part name',
|
help_text=_('Part name'),
|
||||||
validators=[validators.validate_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',
|
variant_of = models.ForeignKey('part.Part', related_name='variants',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@ -337,28 +337,28 @@ class Part(models.Model):
|
|||||||
'active': True,
|
'active': True,
|
||||||
},
|
},
|
||||||
on_delete=models.SET_NULL,
|
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',
|
category = TreeForeignKey(PartCategory, related_name='parts',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
on_delete=models.DO_NOTHING,
|
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)
|
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,
|
default_location = TreeForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
help_text='Where is this item normally stored?',
|
help_text=_('Where is this item normally stored?'),
|
||||||
related_name='default_parts')
|
related_name='default_parts')
|
||||||
|
|
||||||
def get_default_location(self):
|
def get_default_location(self):
|
||||||
@ -402,30 +402,30 @@ class Part(models.Model):
|
|||||||
default_supplier = models.ForeignKey(SupplierPart,
|
default_supplier = models.ForeignKey(SupplierPart,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
help_text='Default supplier part',
|
help_text=_('Default supplier part'),
|
||||||
related_name='default_parts')
|
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)
|
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,
|
bom_checked_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
related_name='boms_checked')
|
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
|
# 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'):
|
for item in self.bom_items.all().prefetch_related('sub_part__stock_items'):
|
||||||
stock = item.sub_part.available_stock
|
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:
|
if total is None or n < total:
|
||||||
total = n
|
total = n
|
||||||
@ -932,9 +932,9 @@ class PartAttachment(models.Model):
|
|||||||
related_name='attachments')
|
related_name='attachments')
|
||||||
|
|
||||||
attachment = models.FileField(upload_to=attach_file,
|
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
|
@property
|
||||||
def basename(self):
|
def basename(self):
|
||||||
@ -994,9 +994,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, 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):
|
class PartParameter(models.Model):
|
||||||
@ -1022,11 +1022,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', 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):
|
class BomItem(models.Model):
|
||||||
@ -1050,7 +1050,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',
|
||||||
help_text='Select parent part',
|
help_text=_('Select parent part'),
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
'assembly': True,
|
'assembly': True,
|
||||||
})
|
})
|
||||||
@ -1058,24 +1058,24 @@ 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',
|
||||||
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.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],
|
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 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):
|
def get_item_hash(self):
|
||||||
""" Calculate the checksum hash of this BOM line item:
|
""" 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(
|
return "{n} x {child} to make {parent}".format(
|
||||||
parent=self.part.full_name,
|
parent=self.part.full_name,
|
||||||
child=self.sub_part.full_name,
|
child=self.sub_part.full_name,
|
||||||
n=self.quantity)
|
n=helpers.decimal2string(self.quantity))
|
||||||
|
|
||||||
def get_overage_quantity(self, quantity):
|
def get_overage_quantity(self, quantity):
|
||||||
""" Calculate overage quantity
|
""" Calculate overage quantity
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends "part/part_base.html" %}
|
{% extends "part/part_base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
{% include 'part/tabs.html' with tab='detail' %}
|
{% include 'part/tabs.html' with tab='detail' %}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
{% extends "part/part_app_base.html" %}
|
{% extends "part/part_app_base.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if part.active == False %}
|
{% if part.active == False %}
|
||||||
<div class='alert alert-danger alert-block'>
|
<div class='alert alert-danger alert-block'>
|
||||||
This part is not active:
|
{% trans "This part is not active" %}"
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.is_template %}
|
{% if part.is_template %}
|
||||||
@ -42,13 +44,13 @@
|
|||||||
<p>
|
<p>
|
||||||
<div class='btn-row'>
|
<div class='btn-row'>
|
||||||
<div class='btn-group'>
|
<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 %}'/>
|
<span id='part-star-icon' class='starred-part glyphicon {% if starred %}glyphicon-star{% else %}glyphicon-star-empty{% endif %}'/>
|
||||||
</button>
|
</button>
|
||||||
{% if part.is_template == False %}
|
{% if part.is_template == False %}
|
||||||
{% include "qr_button.html" %}
|
{% include "qr_button.html" %}
|
||||||
{% if part.active %}
|
{% 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'/>
|
<span id='part-price-icon' class='part-price glyphicon glyphicon-usd'/>
|
||||||
</button>
|
</button>
|
||||||
{% if not part.virtual %}
|
{% if not part.virtual %}
|
||||||
@ -80,13 +82,13 @@
|
|||||||
<table class='table table-condensed'>
|
<table class='table table-condensed'>
|
||||||
{% if part.IPN %}
|
{% if part.IPN %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>IPN</td>
|
<td>{% trans "IPN" %}</td>
|
||||||
<td>{{ part.IPN }}</td>
|
<td>{{ part.IPN }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.URL %}
|
{% if part.URL %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>URL</td>
|
<td>{% trans "URL" %}</td>
|
||||||
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
|
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -100,25 +102,25 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<h4>Available Stock</h4>
|
<h4>{% trans "Available Stock" %}</h4>
|
||||||
</td>
|
</td>
|
||||||
<td><h4>{{ part.available_stock }} {{ part.units }}</h4></td>
|
<td><h4>{% decimal part.available_stock %} {{ part.units }}</h4></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>In Stock</td>
|
<td>{% trans "In Stock" %}</td>
|
||||||
<td>{{ part.total_stock }}</td>
|
<td>{% decimal part.total_stock %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if not part.is_template %}
|
{% if not part.is_template %}
|
||||||
{% if part.allocation_count > 0 %}
|
{% if part.allocation_count > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Allocated</td>
|
<td>{% trans "Allocated" %}</td>
|
||||||
<td>{{ part.allocation_count }}</td>
|
<td>{% decimal part.allocation_count %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.on_order > 0 %}
|
{% if part.on_order > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>On Order</td>
|
<td>{% trans "On Order" %}</td>
|
||||||
<td>{{ part.on_order }}</td>
|
<td>{% decimal part.on_order %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -126,17 +128,17 @@
|
|||||||
{% if part.assembly %}
|
{% if part.assembly %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan='2'>
|
<td colspan='2'>
|
||||||
<b>Build Status</b>
|
<b>{% trans "Build Status" %}</b>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Can Build</td>
|
<td>{% trans "Can Build" %}</td>
|
||||||
<td>{{ part.can_build }}</td>
|
<td>{% decimal part.can_build %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if part.quantity_being_built > 0 %}
|
{% if part.quantity_being_built > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Underway</td>
|
<td>{% trans "Underway" %}</td>
|
||||||
<td>{{ part.quantity_being_built }}</td>
|
<td>{% decimal part.quantity_being_built %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,54 +1,57 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}>
|
<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>
|
||||||
<li{% ifequal tab 'params' %} class='active'{% endifequal %}>
|
<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>
|
</li>
|
||||||
{% if part.is_template %}
|
{% if part.is_template %}
|
||||||
<li{% ifequal tab 'variants' %} class='active'{% endifequal %}>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
|
<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>
|
</li>
|
||||||
{% if part.component or part.used_in_count > 0 %}
|
{% if part.component or part.used_in_count > 0 %}
|
||||||
<li{% ifequal tab 'allocation' %} class="active"{% endifequal %}>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.assembly %}
|
{% if part.assembly %}
|
||||||
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}>
|
<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 %}>
|
<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 %}
|
{% endif %}
|
||||||
{% if part.component or part.used_in_count > 0 %}
|
{% if part.component or part.used_in_count > 0 %}
|
||||||
<li{% ifequal tab 'used' %} class="active"{% endifequal %}>
|
<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 %}
|
{% endif %}
|
||||||
{% if part.purchaseable %}
|
{% if part.purchaseable %}
|
||||||
{% if part.is_template == False %}
|
{% if part.is_template == False %}
|
||||||
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
|
<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>
|
<span class="badge">{{ part.supplier_count }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li{% ifequal tab 'orders' %} class='active'{% endifequal %}>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.trackable and 0 %}
|
{% if part.trackable and 0 %}
|
||||||
<li{% ifequal tab 'track' %} class="active"{% endifequal %}>
|
<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 %}
|
{% if parts.serials.all|length > 0 %}
|
||||||
<span class="badge">{{ part.serials.all|length }}</span>
|
<span class="badge">{{ part.serials.all|length }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a></li>
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
@ -4,10 +4,18 @@ over and above the built-in Django tags.
|
|||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from InvenTree import version
|
from InvenTree import version
|
||||||
|
from InvenTree.helpers import decimal2string
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def decimal(x, *args, **kwargs):
|
||||||
|
""" Simplified rendering of a decimal number """
|
||||||
|
|
||||||
|
return decimal2string(x)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inrange(n, *args, **kwargs):
|
def inrange(n, *args, **kwargs):
|
||||||
""" Return range(n) for iterating through a numeric quantity """
|
""" Return range(n) for iterating through a numeric quantity """
|
||||||
|
@ -123,7 +123,7 @@ class PartAPITest(APITestCase):
|
|||||||
url = reverse('api-bom-item-detail', kwargs={'pk': 3})
|
url = reverse('api-bom-item-detail', kwargs={'pk': 3})
|
||||||
response = self.client.get(url, format='json')
|
response = self.client.get(url, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
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
|
# Increase the quantity
|
||||||
data = response.data
|
data = response.data
|
||||||
@ -134,7 +134,7 @@ class PartAPITest(APITestCase):
|
|||||||
|
|
||||||
# Check that the quantity was increased and a note added
|
# Check that the quantity was increased and a note added
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
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')
|
self.assertEqual(response.data['note'], 'Added a note')
|
||||||
|
|
||||||
def test_add_bom_item(self):
|
def test_add_bom_item(self):
|
||||||
|
19
InvenTree/stock/migrations/0016_auto_20191118_2146.py
Normal file
19
InvenTree/stock/migrations/0016_auto_20191118_2146.py
Normal 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)]),
|
||||||
|
),
|
||||||
|
]
|
19
InvenTree/stock/migrations/0017_auto_20191118_2311.py
Normal file
19
InvenTree/stock/migrations/0017_auto_20191118_2311.py
Normal 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)]),
|
||||||
|
),
|
||||||
|
]
|
@ -18,6 +18,7 @@ from django.dispatch import receiver
|
|||||||
|
|
||||||
from mptt.models import TreeForeignKey
|
from mptt.models import TreeForeignKey
|
||||||
|
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from InvenTree import helpers
|
from InvenTree import helpers
|
||||||
|
|
||||||
@ -294,43 +295,43 @@ class StockItem(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
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={
|
limit_choices_to={
|
||||||
'is_template': False,
|
'is_template': False,
|
||||||
'active': True,
|
'active': True,
|
||||||
})
|
})
|
||||||
|
|
||||||
supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
|
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,
|
location = TreeForeignKey(StockLocation, on_delete=models.DO_NOTHING,
|
||||||
related_name='stock_items', blank=True, null=True,
|
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,
|
belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING,
|
||||||
related_name='owned_parts', blank=True, null=True,
|
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,
|
customer = models.ForeignKey('company.Company', on_delete=models.SET_NULL,
|
||||||
related_name='stockitems', blank=True, null=True,
|
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,
|
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)
|
URL = InvenTreeURLField(max_length=125, blank=True)
|
||||||
|
|
||||||
batch = models.CharField(max_length=100, blank=True, null=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)
|
updated = models.DateField(auto_now=True, null=True)
|
||||||
|
|
||||||
build = models.ForeignKey(
|
build = models.ForeignKey(
|
||||||
'build.Build', on_delete=models.SET_NULL,
|
'build.Build', on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
help_text='Build for this stock item',
|
help_text=_('Build for this stock item'),
|
||||||
related_name='build_outputs',
|
related_name='build_outputs',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -339,7 +340,7 @@ class StockItem(models.Model):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name='stock_items',
|
related_name='stock_items',
|
||||||
blank=True, null=True,
|
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
|
# last time the stock was checked / counted
|
||||||
@ -350,7 +351,7 @@ class StockItem(models.Model):
|
|||||||
|
|
||||||
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, help_text=_('Delete this Stock Item when stock is depleted'))
|
||||||
|
|
||||||
status = models.PositiveIntegerField(
|
status = models.PositiveIntegerField(
|
||||||
default=StockStatus.OK,
|
default=StockStatus.OK,
|
||||||
@ -510,6 +511,11 @@ class StockItem(models.Model):
|
|||||||
if self.serialized:
|
if self.serialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
quantity = Decimal(quantity)
|
||||||
|
except (InvalidOperation, ValueError):
|
||||||
|
return
|
||||||
|
|
||||||
# Doesn't make sense for a zero quantity
|
# Doesn't make sense for a zero quantity
|
||||||
if quantity <= 0:
|
if quantity <= 0:
|
||||||
return
|
return
|
||||||
@ -549,7 +555,10 @@ class StockItem(models.Model):
|
|||||||
quantity: If provided, override the quantity (default = total stock quantity)
|
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:
|
if quantity <= 0:
|
||||||
return False
|
return False
|
||||||
@ -599,12 +608,19 @@ class StockItem(models.Model):
|
|||||||
if self.serialized:
|
if self.serialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.quantity = Decimal(quantity)
|
||||||
|
except (InvalidOperation, ValueError):
|
||||||
|
return
|
||||||
|
|
||||||
if quantity < 0:
|
if quantity < 0:
|
||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
self.quantity = quantity
|
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()
|
self.delete()
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -618,7 +634,10 @@ class StockItem(models.Model):
|
|||||||
record the date of stocktake
|
record the date of stocktake
|
||||||
"""
|
"""
|
||||||
|
|
||||||
count = int(count)
|
try:
|
||||||
|
count = Decimal(count)
|
||||||
|
except InvalidOperation:
|
||||||
|
return False
|
||||||
|
|
||||||
if count < 0 or self.infinite:
|
if count < 0 or self.infinite:
|
||||||
return False
|
return False
|
||||||
@ -646,7 +665,10 @@ class StockItem(models.Model):
|
|||||||
if self.serialized:
|
if self.serialized:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
quantity = int(quantity)
|
try:
|
||||||
|
quantity = Decimal(quantity)
|
||||||
|
except InvalidOperation:
|
||||||
|
return False
|
||||||
|
|
||||||
# Ignore amounts that do not make sense
|
# Ignore amounts that do not make sense
|
||||||
if quantity <= 0 or self.infinite:
|
if quantity <= 0 or self.infinite:
|
||||||
@ -670,7 +692,10 @@ class StockItem(models.Model):
|
|||||||
if self.serialized:
|
if self.serialized:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
quantity = int(quantity)
|
try:
|
||||||
|
quantity = Decimal(quantity)
|
||||||
|
except InvalidOperation:
|
||||||
|
return False
|
||||||
|
|
||||||
if quantity <= 0 or self.infinite:
|
if quantity <= 0 or self.infinite:
|
||||||
return False
|
return False
|
||||||
@ -691,7 +716,7 @@ class StockItem(models.Model):
|
|||||||
sn=self.serial)
|
sn=self.serial)
|
||||||
else:
|
else:
|
||||||
s = '{n} x {part}'.format(
|
s = '{n} x {part}'.format(
|
||||||
n=self.quantity,
|
n=helpers.decimal2string(self.quantity),
|
||||||
part=self.part.full_name)
|
part=self.part.full_name)
|
||||||
|
|
||||||
if self.location:
|
if self.location:
|
||||||
@ -722,17 +747,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, 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)
|
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.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
|
quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends "stock/stock_app_base.html" %}
|
{% extends "stock/stock_app_base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -86,7 +87,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans "Quantity" %}</td>
|
<td>{% trans "Quantity" %}</td>
|
||||||
<td>{{ item.quantity }}</td>
|
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.batch %}
|
{% if item.batch %}
|
||||||
|
@ -245,7 +245,7 @@ class StockTest(TestCase):
|
|||||||
w1 = StockItem.objects.get(pk=100)
|
w1 = StockItem.objects.get(pk=100)
|
||||||
w2 = StockItem.objects.get(pk=101)
|
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')
|
w1.take_stock(30, None, notes='Took 30')
|
||||||
|
|
||||||
# Get from database again
|
# Get from database again
|
||||||
|
@ -20,6 +20,8 @@ from InvenTree.views import QRCodeView
|
|||||||
|
|
||||||
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
|
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
|
||||||
from InvenTree.helpers import ExtractSerialNumbers
|
from InvenTree.helpers import ExtractSerialNumbers
|
||||||
|
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from company.models import Company
|
from company.models import Company
|
||||||
@ -91,7 +93,7 @@ class StockLocationEdit(AjaxUpdateView):
|
|||||||
form_class = EditStockLocationForm
|
form_class = EditStockLocationForm
|
||||||
context_object_name = 'location'
|
context_object_name = 'location'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Stock Location'
|
ajax_form_title = _('Edit Stock Location')
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
""" Customize form data for StockLocation editing.
|
""" Customize form data for StockLocation editing.
|
||||||
@ -115,7 +117,7 @@ class StockLocationEdit(AjaxUpdateView):
|
|||||||
class StockLocationQRCode(QRCodeView):
|
class StockLocationQRCode(QRCodeView):
|
||||||
""" View for displaying a QR code for a StockLocation object """
|
""" 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):
|
def get_qr_data(self):
|
||||||
""" Generate QR code data for the StockLocation """
|
""" Generate QR code data for the StockLocation """
|
||||||
@ -130,7 +132,7 @@ class StockExportOptions(AjaxView):
|
|||||||
""" Form for selecting StockExport options """
|
""" Form for selecting StockExport options """
|
||||||
|
|
||||||
model = StockLocation
|
model = StockLocation
|
||||||
ajax_form_title = 'Stock Export Options'
|
ajax_form_title = _('Stock Export Options')
|
||||||
form_class = ExportOptionsForm
|
form_class = ExportOptionsForm
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@ -238,7 +240,7 @@ class StockExport(AjaxView):
|
|||||||
class StockItemQRCode(QRCodeView):
|
class StockItemQRCode(QRCodeView):
|
||||||
""" View for displaying a QR code for a StockItem object """
|
""" 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):
|
def get_qr_data(self):
|
||||||
""" Generate QR code data for the StockItem """
|
""" Generate QR code data for the StockItem """
|
||||||
@ -261,7 +263,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
ajax_template_name = 'stock/stock_adjust.html'
|
ajax_template_name = 'stock/stock_adjust.html'
|
||||||
ajax_form_title = 'Adjust Stock'
|
ajax_form_title = _('Adjust Stock')
|
||||||
form_class = AdjustStockForm
|
form_class = AdjustStockForm
|
||||||
stock_items = []
|
stock_items = []
|
||||||
|
|
||||||
@ -398,8 +400,9 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
valid = form.is_valid()
|
valid = form.is_valid()
|
||||||
|
|
||||||
for item in self.stock_items:
|
for item in self.stock_items:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item.new_quantity = int(item.new_quantity)
|
item.new_quantity = Decimal(item.new_quantity)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
item.error = _('Must enter integer value')
|
item.error = _('Must enter integer value')
|
||||||
valid = False
|
valid = False
|
||||||
@ -543,7 +546,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
if destination == item.location and item.new_quantity == item.quantity:
|
if destination == item.location and item.new_quantity == item.quantity:
|
||||||
continue
|
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
|
count += 1
|
||||||
|
|
||||||
@ -582,7 +585,7 @@ class StockItemEdit(AjaxUpdateView):
|
|||||||
form_class = EditStockItemForm
|
form_class = EditStockItemForm
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Stock Item'
|
ajax_form_title = _('Edit Stock Item')
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
""" Get form for StockItem editing.
|
""" Get form for StockItem editing.
|
||||||
@ -618,7 +621,7 @@ class StockLocationCreate(AjaxCreateView):
|
|||||||
form_class = EditStockLocationForm
|
form_class = EditStockLocationForm
|
||||||
context_object_name = 'location'
|
context_object_name = 'location'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Create new Stock Location'
|
ajax_form_title = _('Create new Stock Location')
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initials = super(StockLocationCreate, self).get_initial().copy()
|
initials = super(StockLocationCreate, self).get_initial().copy()
|
||||||
@ -639,7 +642,7 @@ class StockItemSerialize(AjaxUpdateView):
|
|||||||
|
|
||||||
model = StockItem
|
model = StockItem
|
||||||
ajax_template_name = 'stock/item_serialize.html'
|
ajax_template_name = 'stock/item_serialize.html'
|
||||||
ajax_form_title = 'Serialize Stock'
|
ajax_form_title = _('Serialize Stock')
|
||||||
form_class = SerializeStockForm
|
form_class = SerializeStockForm
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
@ -719,7 +722,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
form_class = CreateStockItemForm
|
form_class = CreateStockItemForm
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Create new Stock Item'
|
ajax_form_title = _('Create new Stock Item')
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
""" Get form for StockItem creation.
|
""" Get form for StockItem creation.
|
||||||
@ -783,7 +786,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
try:
|
try:
|
||||||
original = StockItem.objects.get(pk=item_to_copy)
|
original = StockItem.objects.get(pk=item_to_copy)
|
||||||
initials = model_to_dict(original)
|
initials = model_to_dict(original)
|
||||||
self.ajax_form_title = "Copy Stock Item"
|
self.ajax_form_title = _("Copy Stock Item")
|
||||||
except StockItem.DoesNotExist:
|
except StockItem.DoesNotExist:
|
||||||
initials = super(StockItemCreate, self).get_initial().copy()
|
initials = super(StockItemCreate, self).get_initial().copy()
|
||||||
|
|
||||||
@ -828,11 +831,12 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
part_id = form['part'].value()
|
part_id = form['part'].value()
|
||||||
try:
|
try:
|
||||||
part = Part.objects.get(id=part_id)
|
part = Part.objects.get(id=part_id)
|
||||||
quantity = int(form['quantity'].value())
|
quantity = Decimal(form['quantity'].value())
|
||||||
except (Part.DoesNotExist, ValueError):
|
except (Part.DoesNotExist, ValueError, InvalidOperation):
|
||||||
part = None
|
part = None
|
||||||
quantity = 1
|
quantity = 1
|
||||||
valid = False
|
valid = False
|
||||||
|
form.errors['quantity'] = [_('Invalid quantity')]
|
||||||
|
|
||||||
if part is None:
|
if part is None:
|
||||||
form.errors['part'] = [_('Invalid part selection')]
|
form.errors['part'] = [_('Invalid part selection')]
|
||||||
@ -914,7 +918,7 @@ class StockLocationDelete(AjaxDeleteView):
|
|||||||
success_url = '/stock'
|
success_url = '/stock'
|
||||||
ajax_template_name = 'stock/location_delete.html'
|
ajax_template_name = 'stock/location_delete.html'
|
||||||
context_object_name = 'location'
|
context_object_name = 'location'
|
||||||
ajax_form_title = 'Delete Stock Location'
|
ajax_form_title = _('Delete Stock Location')
|
||||||
|
|
||||||
|
|
||||||
class StockItemDelete(AjaxDeleteView):
|
class StockItemDelete(AjaxDeleteView):
|
||||||
@ -927,7 +931,7 @@ class StockItemDelete(AjaxDeleteView):
|
|||||||
success_url = '/stock/'
|
success_url = '/stock/'
|
||||||
ajax_template_name = 'stock/item_delete.html'
|
ajax_template_name = 'stock/item_delete.html'
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_form_title = 'Delete Stock Item'
|
ajax_form_title = _('Delete Stock Item')
|
||||||
|
|
||||||
|
|
||||||
class StockItemTrackingDelete(AjaxDeleteView):
|
class StockItemTrackingDelete(AjaxDeleteView):
|
||||||
@ -938,7 +942,7 @@ class StockItemTrackingDelete(AjaxDeleteView):
|
|||||||
|
|
||||||
model = StockItemTracking
|
model = StockItemTracking
|
||||||
ajax_template_name = 'stock/tracking_delete.html'
|
ajax_template_name = 'stock/tracking_delete.html'
|
||||||
ajax_form_title = 'Delete Stock Tracking Entry'
|
ajax_form_title = _('Delete Stock Tracking Entry')
|
||||||
|
|
||||||
|
|
||||||
class StockTrackingIndex(ListView):
|
class StockTrackingIndex(ListView):
|
||||||
@ -955,7 +959,7 @@ class StockItemTrackingEdit(AjaxUpdateView):
|
|||||||
""" View for editing a StockItemTracking object """
|
""" View for editing a StockItemTracking object """
|
||||||
|
|
||||||
model = StockItemTracking
|
model = StockItemTracking
|
||||||
ajax_form_title = 'Edit Stock Tracking Entry'
|
ajax_form_title = _('Edit Stock Tracking Entry')
|
||||||
form_class = TrackingEntryForm
|
form_class = TrackingEntryForm
|
||||||
|
|
||||||
|
|
||||||
@ -964,7 +968,7 @@ class StockItemTrackingCreate(AjaxCreateView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
model = StockItemTracking
|
model = StockItemTracking
|
||||||
ajax_form_title = "Add Stock Tracking Entry"
|
ajax_form_title = _("Add Stock Tracking Entry")
|
||||||
form_class = TrackingEntryForm
|
form_class = TrackingEntryForm
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
Loading…
Reference in New Issue
Block a user