Merge pull request #1309 from SchrodingersGat/order-requirement

Order requirement
This commit is contained in:
Oliver 2021-02-17 14:42:17 +11:00 committed by GitHub
commit 69708b842c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 217 additions and 50 deletions

View File

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

View File

@ -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):
""" """

View File

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

View File

@ -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):
""" """

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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