2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 12:35:46 +00:00

Merge remote-tracking branch 'inventree/master' into partial-shipment

# Conflicts:
#	InvenTree/InvenTree/version.py
#	InvenTree/order/models.py
This commit is contained in:
Oliver
2021-11-11 12:35:59 +11:00
120 changed files with 3551 additions and 1760 deletions

View File

@ -9,16 +9,16 @@ import decimal
import os
from datetime import datetime
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Sum, Q
from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator
from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from markdownx.models import MarkdownxField
@ -27,16 +27,17 @@ from mptt.exceptions import InvalidMove
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
from InvenTree.validators import validate_build_order_reference
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
from InvenTree.validators import validate_build_order_reference
import common.models
import InvenTree.fields
import InvenTree.helpers
import InvenTree.tasks
from stock import models as StockModels
from part import models as PartModels
from stock import models as StockModels
from users import models as UserModels
@ -46,7 +47,7 @@ def get_next_build_number():
"""
if Build.objects.count() == 0:
return
return '0001'
build = Build.objects.exclude(reference=None).last()
@ -99,13 +100,28 @@ class Build(MPTTModel, ReferenceIndexingMixin):
return reverse('api-build-list')
def api_instance_filters(self):
return {
'parent': {
'exclude_tree': self.pk,
}
}
@classmethod
def api_defaults(cls, request):
"""
Return default values for this model when issuing an API OPTIONS request
"""
defaults = {
'reference': get_next_build_number(),
}
if request and request.user:
defaults['issued_by'] = request.user.pk
return defaults
def save(self, *args, **kwargs):
self.rebuild_reference_field()
@ -1014,6 +1030,19 @@ class Build(MPTTModel, ReferenceIndexingMixin):
return self.status == BuildStatus.COMPLETE
@receiver(post_save, sender=Build, dispatch_uid='build_post_save_log')
def after_save_build(sender, instance: Build, created: bool, **kwargs):
"""
Callback function to be executed after a Build instance is saved
"""
if created:
# A new Build has just been created
# Run checks on required parts
InvenTree.tasks.offload_task('build.tasks.check_build_stock', instance)
class BuildOrderAttachment(InvenTreeAttachment):
"""
Model for storing file attachments against a BuildOrder object

96
InvenTree/build/tasks.py Normal file
View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal
import logging
from django.utils.translation import ugettext_lazy as _
from django.template.loader import render_to_string
from allauth.account.models import EmailAddress
import build.models
import InvenTree.helpers
import InvenTree.tasks
import part.models as part_models
logger = logging.getLogger('inventree')
def check_build_stock(build: build.models.Build):
"""
Check the required stock for a newly created build order,
and send an email out to any subscribed users if stock is low.
"""
# Iterate through each of the parts required for this build
lines = []
if not build:
logger.error("Invalid build passed to 'build.tasks.check_build_stock'")
return
try:
part = build.part
except part_models.Part.DoesNotExist:
# Note: This error may be thrown during unit testing...
logger.error("Invalid build.part passed to 'build.tasks.check_build_stock'")
return
for bom_item in part.get_bom_items():
sub_part = bom_item.sub_part
# The 'in stock' quantity depends on whether the bom_item allows variants
in_stock = sub_part.get_stock_count(include_variants=bom_item.allow_variants)
allocated = sub_part.allocation_count()
available = max(0, in_stock - allocated)
required = Decimal(bom_item.quantity) * Decimal(build.quantity)
if available < required:
# There is not sufficient stock for this part
lines.append({
'link': InvenTree.helpers.construct_absolute_url(sub_part.get_absolute_url()),
'part': sub_part,
'in_stock': in_stock,
'allocated': allocated,
'available': available,
'required': required,
})
if len(lines) == 0:
# Nothing to do
return
# Are there any users subscribed to these parts?
subscribers = build.part.get_subscribers()
emails = EmailAddress.objects.filter(
user__in=subscribers,
)
if len(emails) > 0:
logger.info(f"Notifying users of stock required for build {build.pk}")
context = {
'link': InvenTree.helpers.construct_absolute_url(build.get_absolute_url()),
'build': build,
'part': build.part,
'lines': lines,
}
# Render the HTML message
html_message = render_to_string('email/build_order_required_stock.html', context)
subject = "[InvenTree] " + _("Stock required for build order")
recipients = emails.values_list('email', flat=True)
InvenTree.tasks.send_email(subject, '', recipients, html_message=html_message)

View File

@ -34,6 +34,7 @@ src="{% static 'img/blank_image.png' %}"
{% include "admin_button.html" with url=url %}
{% endif %}
<!-- Printing options -->
{% if report_enabled %}
<div class='btn-group'>
<button id='print-options' title='{% trans "Print actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
<span class='fas fa-print'></span> <span class='caret'></span>
@ -42,6 +43,7 @@ src="{% static 'img/blank_image.png' %}"
<li><a class='dropdown-item' href='#' id='print-build-report'><span class='fas fa-file-pdf'></span> {% trans "Print build order report" %}</a></li>
</ul>
</div>
{% endif %}
<!-- Build actions -->
{% if roles.build.change %}
<div class='btn-group'>
@ -224,9 +226,11 @@ src="{% static 'img/blank_image.png' %}"
{% endif %}
});
{% if report_enabled %}
$('#print-build-report').click(function() {
printBuildReports([{{ build.pk }}]);
});
{% endif %}
$("#build-delete").on('click', function() {
launchModalForm(

View File

@ -142,7 +142,7 @@
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Completed" %}</td>
{% if build.completion_date %}
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge badge-right rounded-pill bg-dark'>{{ build.completed_by }}</span>{% endif %}</td>
{% else %}
<td><em>{% trans "Build not complete" %}</em></td>
{% endif %}
@ -160,9 +160,7 @@
<div class='panel-content'>
<div id='child-button-toolbar'>
<div class='button-toolbar container-fluid float-right'>
<div class='filter-list' id='filter-list-sub-build'>
<!-- Empty div for filters -->
</div>
{% include "filter_list.html" with id='sub-build' %}
</div>
</div>
<table class='table table-striped table-condensed' id='sub-build-table' data-toolbar='#child-button-toolbar'></table>
@ -171,7 +169,7 @@
<div class='panel panel-hidden' id='panel-allocate'>
<div class='panel-heading'>
<div class='d-flex flex-row'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Allocate Stock to Build" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
@ -210,9 +208,7 @@
<button id='allocate-selected-items' class='btn btn-success' title='{% trans "Allocate selected items" %}'>
<span class='fas fa-sign-in-alt'></span>
</button>
<div class='filter-list' id='filter-list-builditems'>
<!-- Empty div for table filters-->
</div>
{% include "filter_list.html" with id='builditems' %}
</div>
</div>
</div>
@ -227,7 +223,7 @@
<div class='panel panel-hidden' id='panel-outputs'>
<div class='panel-heading'>
<div class='d-flex flex-row'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Incomplete Build Outputs" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
@ -251,7 +247,9 @@
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li><a class='dropdown-item' href='#' id='multi-output-complete' title='{% trans "Complete selected items" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Complete outputs" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-output-complete' title='{% trans "Complete selected items" %}'>
<span class='fas fa-check-circle icon-green'></span> {% trans "Complete outputs" %}
</a></li>
</ul>
</div>
</div>
@ -276,7 +274,7 @@
<div class='panel panel-hidden' id='panel-attachments'>
<div class='panel-heading'>
<div class='d-flex flex-row'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Attachments" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>

View File

@ -27,6 +27,7 @@
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group' role='group'>
{% if report_enabled %}
<div class='btn-group' role='group'>
<!-- Print actions -->
<button id='build-print-options' class='btn btn-primary dropdown-toggle' data-bs-toggle='dropdown'>
@ -38,6 +39,7 @@
</a></li>
</ul>
</div>
{% endif %}
<!-- Buttons to switch between list and calendar views -->
<button class='btn btn-outline-secondary' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
<span class='fas fa-calendar-alt'></span>
@ -45,9 +47,7 @@
<button class='btn btn-outline-secondary' type='button' id='view-list' title='{% trans "Display list view" %}'>
<span class='fas fa-th-list'></span>
</button>
<div class='filter-list' id='filter-list-build'>
<!-- An empty div in which the filter list will be constructed -->
</div>
{% include "filter_list.html" with id="build" %}
</div>
</div>
</div>
@ -183,6 +183,7 @@ loadBuildTable($("#build-table"), {
url: "{% url 'api-build-list' %}",
});
{% if report_enabled %}
$('#multi-build-print').click(function() {
var rows = $("#build-table").bootstrapTable('getSelections');
@ -194,5 +195,6 @@ $('#multi-build-print').click(function() {
printBuildReports(build_ids);
});
{% endif %}
{% endblock %}