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.')
|
||||
)
|
||||
|
||||
quantity = RoundingDecimalFormField(
|
||||
max_digits=10, decimal_places=5,
|
||||
help_text=_('Number of items to build')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Build
|
||||
fields = [
|
||||
|
@ -23,7 +23,7 @@ from markdownx.models import MarkdownxField
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
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.models import InvenTreeAttachment
|
||||
|
||||
@ -65,6 +65,20 @@ class Build(MPTTModel):
|
||||
verbose_name = _("Build Order")
|
||||
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
|
||||
def filterByDate(queryset, min_date, max_date):
|
||||
"""
|
||||
|
@ -675,6 +675,13 @@ class BuildCreate(AjaxCreateView):
|
||||
|
||||
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)
|
||||
|
||||
if part:
|
||||
@ -684,18 +691,18 @@ class BuildCreate(AjaxCreateView):
|
||||
# User has provided a Part ID
|
||||
initials['part'] = part
|
||||
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):
|
||||
pass
|
||||
|
||||
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
|
||||
initials['issued_by'] = self.request.user
|
||||
|
||||
|
@ -41,7 +41,7 @@ from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
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 order import models as OrderModels
|
||||
@ -884,20 +884,135 @@ class Part(MPTTModel):
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
# How many do we need to have "on hand" at any point?
|
||||
required = self.net_stock - self.minimum_stock
|
||||
Here, an "order" could be one of:
|
||||
- Build Order
|
||||
- Sales Order
|
||||
|
||||
if required < 0:
|
||||
return abs(required)
|
||||
To work out how many we need to order:
|
||||
|
||||
# Do not need to order any
|
||||
return 0
|
||||
Stock on hand = self.total_stock
|
||||
Required for orders = self.required_order_quantity()
|
||||
Currently on order = self.on_order
|
||||
Currently building = self.quantity_being_built
|
||||
|
||||
"""
|
||||
|
||||
# Total requirement
|
||||
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)
|
||||
|
||||
@property
|
||||
@ -979,16 +1094,22 @@ class Part(MPTTModel):
|
||||
|
||||
@property
|
||||
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=Coalesce(Sum('quantity'), Decimal(0))
|
||||
)
|
||||
quantity = 0
|
||||
|
||||
return query['quantity']
|
||||
for build in builds:
|
||||
# The remaining items in the build
|
||||
quantity += build.remaining
|
||||
|
||||
return quantity
|
||||
|
||||
def build_order_allocations(self):
|
||||
"""
|
||||
|
@ -119,36 +119,35 @@
|
||||
<td>
|
||||
<h4>{% trans "Available Stock" %}</h4>
|
||||
</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>
|
||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||
<td>{% trans "In Stock" %}</td>
|
||||
<td>{% include "part/stock_count.html" %}</td>
|
||||
</tr>
|
||||
{% if not part.is_template %}
|
||||
{% 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 %}
|
||||
{% if on_order > 0 %}
|
||||
<tr>
|
||||
<td><span class='fas fa-shopping-cart'></span></td>
|
||||
<td>{% trans "On Order" %}</td>
|
||||
<td>{% decimal part.on_order %}</td>
|
||||
<td>{% decimal on_order %}</td>
|
||||
</tr>
|
||||
{% 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 %}
|
||||
{% 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 part.assembly %}
|
||||
<tr>
|
||||
@ -162,11 +161,11 @@
|
||||
<td>{% trans "Can Build" %}</td>
|
||||
<td>{% decimal part.can_build %}</td>
|
||||
</tr>
|
||||
{% if part.quantity_being_built > 0 %}
|
||||
{% if quantity_being_built > 0 %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Underway" %}</td>
|
||||
<td>{% decimal part.quantity_being_built %}</td>
|
||||
<td>{% trans "Building" %}</td>
|
||||
<td>{% decimal quantity_being_built %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -1,10 +1,10 @@
|
||||
{% load inventree_extras %}
|
||||
{% 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>
|
||||
{% elif part.total_stock < part.minimum_stock %}
|
||||
{% elif total_stock < part.minimum_stock %}
|
||||
<span class='label label-warning label-right'>{% trans "Low Stock" %}</span>
|
||||
{% endif %}
|
@ -15,12 +15,12 @@
|
||||
{% endif %}
|
||||
{% if not part.virtual %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if part.component or part.salable or part.used_in_count > 0 %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if part.assembly %}
|
||||
|
@ -792,6 +792,22 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
|
||||
context['starred'] = part.isStarredBy(self.request.user)
|
||||
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
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
{% load report %}
|
||||
{% load inventree_extras %}
|
||||
{% load markdownify %}
|
||||
{% load qr_code %}
|
||||
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
|
@ -110,7 +110,7 @@ class StockTest(TestCase):
|
||||
# The "is_building" quantity should not be counted here
|
||||
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):
|
||||
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
|
||||
*/
|
||||
|
||||
if (typeof table === 'string') {
|
||||
table = $(table);
|
||||
}
|
||||
|
||||
// Initially set the enable state of the buttons
|
||||
enableButtons(buttons, table.bootstrapTable('getSelections').length > 0);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user