mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1309 from SchrodingersGat/order-requirement
Order requirement
This commit is contained in:
commit
69708b842c
@ -39,6 +39,11 @@ class EditBuildForm(HelperForm):
|
|||||||
help_text=_('Target date for build completion. Build will be overdue after this date.')
|
help_text=_('Target date for build completion. Build will be overdue after this date.')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
quantity = RoundingDecimalFormField(
|
||||||
|
max_digits=10, decimal_places=5,
|
||||||
|
help_text=_('Number of items to build')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Build
|
model = Build
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -23,7 +23,7 @@ from markdownx.models import MarkdownxField
|
|||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
from InvenTree.helpers import increment, getSetting, normalize
|
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
||||||
from InvenTree.validators import validate_build_order_reference
|
from InvenTree.validators import validate_build_order_reference
|
||||||
from InvenTree.models import InvenTreeAttachment
|
from InvenTree.models import InvenTreeAttachment
|
||||||
|
|
||||||
@ -65,6 +65,20 @@ class Build(MPTTModel):
|
|||||||
verbose_name = _("Build Order")
|
verbose_name = _("Build Order")
|
||||||
verbose_name_plural = _("Build Orders")
|
verbose_name_plural = _("Build Orders")
|
||||||
|
|
||||||
|
def format_barcode(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a JSON string to represent this build as a barcode
|
||||||
|
"""
|
||||||
|
|
||||||
|
return MakeBarcode(
|
||||||
|
"buildorder",
|
||||||
|
self.pk,
|
||||||
|
{
|
||||||
|
"reference": self.title,
|
||||||
|
"url": self.get_absolute_url(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filterByDate(queryset, min_date, max_date):
|
def filterByDate(queryset, min_date, max_date):
|
||||||
"""
|
"""
|
||||||
|
@ -675,6 +675,13 @@ class BuildCreate(AjaxCreateView):
|
|||||||
|
|
||||||
initials = super(BuildCreate, self).get_initial().copy()
|
initials = super(BuildCreate, self).get_initial().copy()
|
||||||
|
|
||||||
|
initials['parent'] = self.request.GET.get('parent', None)
|
||||||
|
|
||||||
|
# User has provided a SalesOrder ID
|
||||||
|
initials['sales_order'] = self.request.GET.get('sales_order', None)
|
||||||
|
|
||||||
|
initials['quantity'] = self.request.GET.get('quantity', 1)
|
||||||
|
|
||||||
part = self.request.GET.get('part', None)
|
part = self.request.GET.get('part', None)
|
||||||
|
|
||||||
if part:
|
if part:
|
||||||
@ -684,18 +691,18 @@ class BuildCreate(AjaxCreateView):
|
|||||||
# User has provided a Part ID
|
# User has provided a Part ID
|
||||||
initials['part'] = part
|
initials['part'] = part
|
||||||
initials['destination'] = part.get_default_location()
|
initials['destination'] = part.get_default_location()
|
||||||
|
|
||||||
|
to_order = part.quantity_to_order
|
||||||
|
|
||||||
|
if to_order < 1:
|
||||||
|
to_order = 1
|
||||||
|
|
||||||
|
initials['quantity'] = to_order
|
||||||
except (ValueError, Part.DoesNotExist):
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
initials['reference'] = Build.getNextBuildNumber()
|
initials['reference'] = Build.getNextBuildNumber()
|
||||||
|
|
||||||
initials['parent'] = self.request.GET.get('parent', None)
|
|
||||||
|
|
||||||
# User has provided a SalesOrder ID
|
|
||||||
initials['sales_order'] = self.request.GET.get('sales_order', None)
|
|
||||||
|
|
||||||
initials['quantity'] = self.request.GET.get('quantity', 1)
|
|
||||||
|
|
||||||
# Pre-fill the issued_by user
|
# Pre-fill the issued_by user
|
||||||
initials['issued_by'] = self.request.user
|
initials['issued_by'] = self.request.user
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
|||||||
from InvenTree.fields import InvenTreeURLField
|
from InvenTree.fields import InvenTreeURLField
|
||||||
from InvenTree.helpers import decimal2string, normalize
|
from InvenTree.helpers import decimal2string, normalize
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
||||||
|
|
||||||
from build import models as BuildModels
|
from build import models as BuildModels
|
||||||
from order import models as OrderModels
|
from order import models as OrderModels
|
||||||
@ -884,20 +884,135 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
return max(total, 0)
|
return max(total, 0)
|
||||||
|
|
||||||
|
def requiring_build_orders(self):
|
||||||
|
"""
|
||||||
|
Return list of outstanding build orders which require this part
|
||||||
|
"""
|
||||||
|
|
||||||
|
# List of BOM that this part is required for
|
||||||
|
boms = BomItem.objects.filter(sub_part=self)
|
||||||
|
|
||||||
|
part_ids = [bom.part.pk for bom in boms]
|
||||||
|
|
||||||
|
# Now, get a list of outstanding build orders which require this part
|
||||||
|
builds = BuildModels.Build.objects.filter(
|
||||||
|
part__in=part_ids,
|
||||||
|
status__in=BuildStatus.ACTIVE_CODES
|
||||||
|
)
|
||||||
|
|
||||||
|
return builds
|
||||||
|
|
||||||
|
def required_build_order_quantity(self):
|
||||||
|
"""
|
||||||
|
Return the quantity of this part required for active build orders
|
||||||
|
"""
|
||||||
|
|
||||||
|
# List of BOM that this part is required for
|
||||||
|
boms = BomItem.objects.filter(sub_part=self)
|
||||||
|
|
||||||
|
part_ids = [bom.part.pk for bom in boms]
|
||||||
|
|
||||||
|
# Now, get a list of outstanding build orders which require this part
|
||||||
|
builds = BuildModels.Build.objects.filter(
|
||||||
|
part__in=part_ids,
|
||||||
|
status__in=BuildStatus.ACTIVE_CODES
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = 0
|
||||||
|
|
||||||
|
for build in builds:
|
||||||
|
|
||||||
|
bom_item = None
|
||||||
|
|
||||||
|
# Match BOM item to build
|
||||||
|
for bom in boms:
|
||||||
|
if bom.part == build.part:
|
||||||
|
bom_item = bom
|
||||||
|
break
|
||||||
|
|
||||||
|
if bom_item is None:
|
||||||
|
logger.warning("Found null BomItem when calculating required quantity")
|
||||||
|
continue
|
||||||
|
|
||||||
|
build_quantity = build.quantity * bom_item.quantity
|
||||||
|
|
||||||
|
quantity += build_quantity
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
def requiring_sales_orders(self):
|
||||||
|
"""
|
||||||
|
Return a list of sales orders which require this part
|
||||||
|
"""
|
||||||
|
|
||||||
|
orders = set()
|
||||||
|
|
||||||
|
# Get a list of line items for open orders which match this part
|
||||||
|
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
||||||
|
order__status__in=SalesOrderStatus.OPEN,
|
||||||
|
part=self
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in open_lines:
|
||||||
|
orders.add(line.order)
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
def required_sales_order_quantity(self):
|
||||||
|
"""
|
||||||
|
Return the quantity of this part required for active sales orders
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get a list of line items for open orders which match this part
|
||||||
|
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
||||||
|
order__status__in=SalesOrderStatus.OPEN,
|
||||||
|
part=self
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = 0
|
||||||
|
|
||||||
|
for line in open_lines:
|
||||||
|
quantity += line.quantity
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
def required_order_quantity(self):
|
||||||
|
"""
|
||||||
|
Return total required to fulfil orders
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.required_build_order_quantity() + self.required_sales_order_quantity()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_to_order(self):
|
def quantity_to_order(self):
|
||||||
""" Return the quantity needing to be ordered for this part. """
|
"""
|
||||||
|
Return the quantity needing to be ordered for this part.
|
||||||
|
|
||||||
|
Here, an "order" could be one of:
|
||||||
|
- Build Order
|
||||||
|
- Sales Order
|
||||||
|
|
||||||
# How many do we need to have "on hand" at any point?
|
To work out how many we need to order:
|
||||||
required = self.net_stock - self.minimum_stock
|
|
||||||
|
|
||||||
if required < 0:
|
Stock on hand = self.total_stock
|
||||||
return abs(required)
|
Required for orders = self.required_order_quantity()
|
||||||
|
Currently on order = self.on_order
|
||||||
|
Currently building = self.quantity_being_built
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
# Do not need to order any
|
# Total requirement
|
||||||
return 0
|
required = self.required_order_quantity()
|
||||||
|
|
||||||
|
# Subtract stock levels
|
||||||
|
required -= max(self.total_stock, self.minimum_stock)
|
||||||
|
|
||||||
|
# Subtract quantity on order
|
||||||
|
required -= self.on_order
|
||||||
|
|
||||||
|
# Subtract quantity being built
|
||||||
|
required -= self.quantity_being_built
|
||||||
|
|
||||||
required = self.net_stock
|
|
||||||
return max(required, 0)
|
return max(required, 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -979,16 +1094,22 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_being_built(self):
|
def quantity_being_built(self):
|
||||||
""" Return the current number of parts currently being built
|
"""
|
||||||
|
Return the current number of parts currently being built.
|
||||||
|
|
||||||
|
Note: This is the total quantity of Build orders, *not* the number of build outputs.
|
||||||
|
In this fashion, it is the "projected" quantity of builds
|
||||||
"""
|
"""
|
||||||
|
|
||||||
stock_items = self.stock_items.filter(is_building=True)
|
builds = self.active_builds
|
||||||
|
|
||||||
query = stock_items.aggregate(
|
quantity = 0
|
||||||
quantity=Coalesce(Sum('quantity'), Decimal(0))
|
|
||||||
)
|
|
||||||
|
|
||||||
return query['quantity']
|
for build in builds:
|
||||||
|
# The remaining items in the build
|
||||||
|
quantity += build.remaining
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
|
||||||
def build_order_allocations(self):
|
def build_order_allocations(self):
|
||||||
"""
|
"""
|
||||||
|
@ -119,36 +119,35 @@
|
|||||||
<td>
|
<td>
|
||||||
<h4>{% trans "Available Stock" %}</h4>
|
<h4>{% trans "Available Stock" %}</h4>
|
||||||
</td>
|
</td>
|
||||||
<td><h4>{% decimal part.available_stock %}{% if part.units %} {{ part.units }}{% endif %}</h4></td>
|
<td><h4>{% decimal available %}{% if part.units %} {{ part.units }}{% endif %}</h4></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||||
<td>{% trans "In Stock" %}</td>
|
<td>{% trans "In Stock" %}</td>
|
||||||
<td>{% include "part/stock_count.html" %}</td>
|
<td>{% include "part/stock_count.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if not part.is_template %}
|
{% if on_order > 0 %}
|
||||||
{% if part.build_order_allocation_count > 0 %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-dolly'></span></td>
|
|
||||||
<td>{% trans "Allocated to Build Orders" %}</td>
|
|
||||||
<td>{% decimal part.build_order_allocation_count %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.sales_order_allocation_count > 0 %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-dolly'></span></td>
|
|
||||||
<td>{% trans "Allocated to Sales Orders" %}</td>
|
|
||||||
<td>{% decimal part.sales_order_allocation_count %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.on_order > 0 %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-shopping-cart'></span></td>
|
<td><span class='fas fa-shopping-cart'></span></td>
|
||||||
<td>{% trans "On Order" %}</td>
|
<td>{% trans "On Order" %}</td>
|
||||||
<td>{% decimal part.on_order %}</td>
|
<td>{% decimal on_order %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if required > 0 %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-clipboard-list'></span></td>
|
||||||
|
<td>{% trans "Required for Orders" %}</td>
|
||||||
|
<td>{% decimal required %}
|
||||||
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if allocated > 0 %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-dolly'></span></td>
|
||||||
|
<td>{% trans "Allocated to Orders" %}</td>
|
||||||
|
<td>{% decimal allocated %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if not part.is_template %}
|
{% if not part.is_template %}
|
||||||
{% if part.assembly %}
|
{% if part.assembly %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -162,11 +161,11 @@
|
|||||||
<td>{% trans "Can Build" %}</td>
|
<td>{% trans "Can Build" %}</td>
|
||||||
<td>{% decimal part.can_build %}</td>
|
<td>{% decimal part.can_build %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if part.quantity_being_built > 0 %}
|
{% if quantity_being_built > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{% trans "Underway" %}</td>
|
<td>{% trans "Building" %}</td>
|
||||||
<td>{% decimal part.quantity_being_built %}</td>
|
<td>{% decimal quantity_being_built %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% decimal part.total_stock %}
|
{% decimal total_stock %}
|
||||||
|
|
||||||
{% if part.total_stock == 0 %}
|
{% if total_stock == 0 %}
|
||||||
<span class='label label-danger label-right'>{% trans "No Stock" %}</span>
|
<span class='label label-danger label-right'>{% trans "No Stock" %}</span>
|
||||||
{% elif part.total_stock < part.minimum_stock %}
|
{% elif total_stock < part.minimum_stock %}
|
||||||
<span class='label label-warning label-right'>{% trans "Low Stock" %}</span>
|
<span class='label label-warning label-right'>{% trans "Low Stock" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
@ -15,12 +15,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not part.virtual %}
|
{% if not part.virtual %}
|
||||||
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
|
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
|
||||||
<a href="{% url 'part-stock' part.id %}">{% trans "Stock" %} <span class="badge">{% decimal part.total_stock %}</span></a>
|
<a href="{% url 'part-stock' part.id %}">{% trans "Stock" %} <span class="badge">{% decimal total_stock %}</span></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.component or part.salable or part.used_in_count > 0 %}
|
{% if part.component or part.salable 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 %}">{% trans "Allocated" %} <span class="badge">{% decimal part.allocation_count %}</span></a>
|
<a href="{% url 'part-allocation' part.id %}">{% trans "Allocated" %} <span class="badge">{% decimal allocated %}</span></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.assembly %}
|
{% if part.assembly %}
|
||||||
|
@ -792,6 +792,22 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
context['starred'] = part.isStarredBy(self.request.user)
|
context['starred'] = part.isStarredBy(self.request.user)
|
||||||
context['disabled'] = not part.active
|
context['disabled'] = not part.active
|
||||||
|
|
||||||
|
# Pre-calculate complex queries so they only need to be performed once
|
||||||
|
context['total_stock'] = part.total_stock
|
||||||
|
|
||||||
|
context['quantity_being_built'] = part.quantity_being_built
|
||||||
|
|
||||||
|
context['required_build_order_quantity'] = part.required_build_order_quantity()
|
||||||
|
context['allocated_build_order_quantity'] = part.build_order_allocation_count()
|
||||||
|
|
||||||
|
context['required_sales_order_quantity'] = part.required_sales_order_quantity()
|
||||||
|
context['allocated_sales_order_quantity'] = part.sales_order_allocation_count()
|
||||||
|
|
||||||
|
context['available'] = part.available_stock
|
||||||
|
context['on_order'] = part.on_order
|
||||||
|
context['required'] = context['required_build_order_quantity'] + context['required_sales_order_quantity']
|
||||||
|
context['allocated'] = context['allocated_build_order_quantity'] + context['allocated_sales_order_quantity']
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
{% load report %}
|
{% load report %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load markdownify %}
|
{% load markdownify %}
|
||||||
|
{% load qr_code %}
|
||||||
|
|
||||||
{% block page_margin %}
|
{% block page_margin %}
|
||||||
margin: 2cm;
|
margin: 2cm;
|
||||||
|
@ -110,7 +110,7 @@ class StockTest(TestCase):
|
|||||||
# The "is_building" quantity should not be counted here
|
# The "is_building" quantity should not be counted here
|
||||||
self.assertEqual(part.total_stock, n + 5)
|
self.assertEqual(part.total_stock, n + 5)
|
||||||
|
|
||||||
self.assertEqual(part.quantity_being_built, 100)
|
self.assertEqual(part.quantity_being_built, 1)
|
||||||
|
|
||||||
def test_loc_count(self):
|
def test_loc_count(self):
|
||||||
self.assertEqual(StockLocation.objects.count(), 7)
|
self.assertEqual(StockLocation.objects.count(), 7)
|
||||||
|
@ -45,6 +45,10 @@ function linkButtonsToSelection(table, buttons) {
|
|||||||
* The buttons will only be enabled if there is at least one row selected
|
* The buttons will only be enabled if there is at least one row selected
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
if (typeof table === 'string') {
|
||||||
|
table = $(table);
|
||||||
|
}
|
||||||
|
|
||||||
// Initially set the enable state of the buttons
|
// Initially set the enable state of the buttons
|
||||||
enableButtons(buttons, table.bootstrapTable('getSelections').length > 0);
|
enableButtons(buttons, table.bootstrapTable('getSelections').length > 0);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user