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:
@ -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
96
InvenTree/build/tasks.py
Normal 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)
|
@ -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(
|
||||
|
@ -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'>
|
||||
|
@ -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 %}
|
Reference in New Issue
Block a user