mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
add report feature for stock locations (#5134)
* add report feature for stock locations * fix flake 8 errors * run pre-commit run --all-files to fix style errors * add new model * create default stock location
This commit is contained in:
parent
9cf629ccc5
commit
cf0d30b11c
@ -4,7 +4,7 @@ from django.contrib import admin
|
||||
|
||||
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
|
||||
ReportAsset, ReportSnippet, ReturnOrderReport,
|
||||
SalesOrderReport, TestReport)
|
||||
SalesOrderReport, StockLocationReport, TestReport)
|
||||
|
||||
|
||||
class ReportTemplateAdmin(admin.ModelAdmin):
|
||||
@ -25,6 +25,7 @@ class ReportAssetAdmin(admin.ModelAdmin):
|
||||
admin.site.register(ReportSnippet, ReportSnippetAdmin)
|
||||
admin.site.register(ReportAsset, ReportAssetAdmin)
|
||||
|
||||
admin.site.register(StockLocationReport, ReportTemplateAdmin)
|
||||
admin.site.register(TestReport, ReportTemplateAdmin)
|
||||
admin.site.register(BuildReport, ReportTemplateAdmin)
|
||||
admin.site.register(BillOfMaterialsReport, ReportTemplateAdmin)
|
||||
|
@ -20,14 +20,16 @@ import part.models
|
||||
from InvenTree.api import MetadataView
|
||||
from InvenTree.filters import InvenTreeSearchFilter
|
||||
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
|
||||
from stock.models import StockItem, StockItemAttachment
|
||||
from stock.models import StockItem, StockItemAttachment, StockLocation
|
||||
|
||||
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
|
||||
ReturnOrderReport, SalesOrderReport, TestReport)
|
||||
ReturnOrderReport, SalesOrderReport, StockLocationReport,
|
||||
TestReport)
|
||||
from .serializers import (BOMReportSerializer, BuildReportSerializer,
|
||||
PurchaseOrderReportSerializer,
|
||||
ReturnOrderReportSerializer,
|
||||
SalesOrderReportSerializer, TestReportSerializer)
|
||||
SalesOrderReportSerializer,
|
||||
StockLocationReportSerializer, TestReportSerializer)
|
||||
|
||||
|
||||
class ReportListView(ListAPI):
|
||||
@ -448,6 +450,30 @@ class ReturnOrderReportPrint(ReturnOrderReportMixin, ReportPrintMixin, RetrieveA
|
||||
pass
|
||||
|
||||
|
||||
class StockLocationReportMixin(ReportFilterMixin):
|
||||
"""Mixin for StockLocation report template"""
|
||||
|
||||
ITEM_MODEL = StockLocation
|
||||
ITEM_KEY = 'location'
|
||||
queryset = StockLocationReport.objects.all()
|
||||
serializer_class = StockLocationReportSerializer
|
||||
|
||||
|
||||
class StockLocationReportList(StockLocationReportMixin, ReportListView):
|
||||
"""API list endpoint for the StockLocationReportList model"""
|
||||
pass
|
||||
|
||||
|
||||
class StockLocationReportDetail(StockLocationReportMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for a single StockLocationReportDetail object."""
|
||||
pass
|
||||
|
||||
|
||||
class StockLocationReportPrint(StockLocationReportMixin, ReportPrintMixin, RetrieveAPI):
|
||||
"""API endpoint for printing a StockLocationReportPrint object"""
|
||||
pass
|
||||
|
||||
|
||||
report_api_urls = [
|
||||
|
||||
# Purchase order reports
|
||||
@ -524,4 +550,18 @@ report_api_urls = [
|
||||
# List view
|
||||
re_path(r'^.*$', StockItemTestReportList.as_view(), name='api-stockitem-testreport-list'),
|
||||
])),
|
||||
|
||||
# Stock Location reports (Stock Location Reports -> sir)
|
||||
re_path(r'slr/', include([
|
||||
# Detail views
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'print/?', StockLocationReportPrint.as_view(), name='api-stocklocation-report-print'),
|
||||
re_path(r'metadata/', MetadataView.as_view(), {'report': StockLocationReport}, name='api-stocklocation-report-metadata'),
|
||||
re_path(r'^.*$', StockLocationReportDetail.as_view(), name='api-stocklocation-report-detail'),
|
||||
])),
|
||||
|
||||
# List view
|
||||
re_path(r'^.*$', StockLocationReportList.as_view(), name='api-stocklocation-report-list'),
|
||||
])),
|
||||
|
||||
]
|
||||
|
@ -32,6 +32,7 @@ class ReportConfig(AppConfig):
|
||||
self.create_default_purchase_order_reports()
|
||||
self.create_default_sales_order_reports()
|
||||
self.create_default_return_order_reports()
|
||||
self.create_default_stock_location_reports()
|
||||
|
||||
def create_default_reports(self, model, reports):
|
||||
"""Copy default report files across to the media directory."""
|
||||
@ -201,3 +202,23 @@ class ReportConfig(AppConfig):
|
||||
]
|
||||
|
||||
self.create_default_reports(ReturnOrderReport, reports)
|
||||
|
||||
def create_default_stock_location_reports(self):
|
||||
"""Create database entries for the default StockLocationReport templates"""
|
||||
|
||||
try:
|
||||
from report.models import StockLocationReport
|
||||
except Exception: # pragma: no cover
|
||||
# Database not yet ready
|
||||
return
|
||||
|
||||
# List of templates to copy across
|
||||
reports = [
|
||||
{
|
||||
'file': 'inventree_slr_report.html',
|
||||
'name': 'InvenTree Stock Location',
|
||||
'description': 'Stock Location example report',
|
||||
}
|
||||
]
|
||||
|
||||
self.create_default_reports(StockLocationReport, reports)
|
||||
|
32
InvenTree/report/migrations/0020_stocklocationreport.py
Normal file
32
InvenTree/report/migrations/0020_stocklocationreport.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Generated by Django 3.2.19 on 2023-06-29 14:46
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import report.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('report', '0019_returnorderreport_metadata'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StockLocationReport',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
|
||||
('name', models.CharField(help_text='Template name', max_length=100, verbose_name='Name')),
|
||||
('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm'])], verbose_name='Template')),
|
||||
('description', models.CharField(help_text='Report template description', max_length=250, verbose_name='Description')),
|
||||
('revision', models.PositiveIntegerField(default=1, editable=False, help_text='Report revision number (auto-increments)', verbose_name='Revision')),
|
||||
('filename_pattern', models.CharField(default='report.pdf', help_text='Pattern for generating report filenames', max_length=100, verbose_name='Filename Pattern')),
|
||||
('enabled', models.BooleanField(default=True, help_text='Report template is enabled', verbose_name='Enabled')),
|
||||
('filters', models.CharField(blank=True, help_text='stock location query filters (comma-separated list of key=value pairs)', max_length=250, validators=[report.models.validate_stock_location_report_filters], verbose_name='Filters')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -74,6 +74,11 @@ def validate_return_order_filters(filters):
|
||||
return validateFilterString(filters, model=order.models.ReturnOrder)
|
||||
|
||||
|
||||
def validate_stock_location_report_filters(filters):
|
||||
"""Validate filter string against StockLocation model."""
|
||||
return validateFilterString(filters, model=stock.models.StockLocation)
|
||||
|
||||
|
||||
class WeasyprintReportMixin(WeasyTemplateResponseMixin):
|
||||
"""Class for rendering a HTML template to a PDF."""
|
||||
|
||||
@ -619,3 +624,39 @@ class ReportAsset(models.Model):
|
||||
verbose_name=_('Description'),
|
||||
help_text=_("Asset file description")
|
||||
)
|
||||
|
||||
|
||||
class StockLocationReport(ReportTemplateBase):
|
||||
"""Render a StockLocationReport against a StockLocation object."""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the StockLocationReport model"""
|
||||
return reverse('api-stocklocation-report-list')
|
||||
|
||||
@classmethod
|
||||
def getSubdir(cls):
|
||||
"""Return the subdirectory where StockLocationReport templates are located"""
|
||||
return 'slr'
|
||||
|
||||
filters = models.CharField(
|
||||
blank=True,
|
||||
max_length=250,
|
||||
verbose_name=_('Filters'),
|
||||
help_text=_("stock location query filters (comma-separated list of key=value pairs)"),
|
||||
validators=[
|
||||
validate_stock_location_report_filters
|
||||
]
|
||||
)
|
||||
|
||||
def get_context_data(self, request):
|
||||
"""Return custom context data for the StockLocationReport template"""
|
||||
stock_location = self.object_to_print
|
||||
|
||||
if type(stock_location) != stock.models.StockLocation:
|
||||
raise TypeError('Provided model is not a StockLocation object -> ' + str(type(stock_location)))
|
||||
|
||||
return {
|
||||
'stock_location': stock_location,
|
||||
'stock_items': stock_location.get_stock_items(),
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
||||
InvenTreeModelSerializer)
|
||||
|
||||
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
|
||||
ReturnOrderReport, SalesOrderReport, TestReport)
|
||||
ReturnOrderReport, SalesOrderReport, StockLocationReport,
|
||||
TestReport)
|
||||
|
||||
|
||||
class ReportSerializerBase(InvenTreeModelSerializer):
|
||||
@ -84,3 +85,13 @@ class ReturnOrderReportSerializer(ReportSerializerBase):
|
||||
|
||||
model = ReturnOrderReport
|
||||
fields = ReportSerializerBase.report_fields()
|
||||
|
||||
|
||||
class StockLocationReportSerializer(ReportSerializerBase):
|
||||
"""Serializer class for the StockLocationReport model"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
|
||||
model = StockLocationReport
|
||||
fields = ReportSerializerBase.report_fields()
|
||||
|
124
InvenTree/report/templates/report/inventree_slr_report.html
Normal file
124
InvenTree/report/templates/report/inventree_slr_report.html
Normal file
@ -0,0 +1,124 @@
|
||||
{% extends "report/inventree_report_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load report %}
|
||||
{% load barcode %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
margin-top: 4cm;
|
||||
{% endblock page_margin %}
|
||||
|
||||
{% block bottom_left %}
|
||||
content: "v{{ report_revision }} - {{ date.isoformat }}";
|
||||
{% endblock bottom_left %}
|
||||
|
||||
{% block bottom_center %}
|
||||
content: "{% inventree_version shortstring=True %}";
|
||||
{% endblock bottom_center %}
|
||||
|
||||
{% block style %}
|
||||
|
||||
.header-right {
|
||||
text-align: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 20mm;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.thumb-container {
|
||||
width: 32px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.part-thumb {
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.part-text {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.part-logo {
|
||||
max-width: 60px;
|
||||
max-height: 60px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 3px;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
table td {
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
table td.shrink {
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
table td.expand {
|
||||
width: 99%
|
||||
}
|
||||
|
||||
.invisible-table {
|
||||
border: 0px solid transparent;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.invisible-table td {
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
|
||||
.main-part-text {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.main-part-description {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
{% endblock style %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<h3>{% trans "Stock location items" %}</h3>
|
||||
<h3>{{ stock_location.name }}</h3>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Part" %}</th>
|
||||
<th>{% trans "IPN" %}</th>
|
||||
<th>{% trans "Quantity" %}</th>
|
||||
<th>{% trans "Note" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in stock_items.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class='part-text'>
|
||||
{{ line.part.full_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ line.part.IPN }}</td>
|
||||
<td>{% decimal line.quantity %}</td>
|
||||
<td>{{ line.notes }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock page_content %}
|
@ -475,3 +475,18 @@ class ReturnOrderReportTest(ReportTest):
|
||||
self.copyReportTemplate('inventree_return_order_report.html', 'return order report')
|
||||
|
||||
return super().setUp()
|
||||
|
||||
|
||||
class StockLocationReportTest(ReportTest):
|
||||
"""Unit tests for the StockLocationReport model"""
|
||||
|
||||
model = report_models.StockLocationReport
|
||||
list_url = 'api-stocklocation-report-list'
|
||||
detail_url = 'api-stocklocation-report-detail'
|
||||
print_url = 'api-stocklocation-report-print'
|
||||
|
||||
def setUp(self):
|
||||
"""Setup function for the StockLocationReport tests"""
|
||||
self.copyReportTemplate('inventree_slr_report.html', 'stock location report')
|
||||
|
||||
return super().setUp()
|
||||
|
@ -70,7 +70,8 @@
|
||||
<span class='fas fa-print'></span> <span class='caret'></span>
|
||||
</button>
|
||||
<ul class='dropdown-menu'>
|
||||
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a>
|
||||
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='print-location-report'><span class='fas fa-tag'></span> {% trans "Print Location Report" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -282,6 +283,17 @@
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if report_enabled %}
|
||||
$('#print-location-report').click(function() {
|
||||
|
||||
printReports({
|
||||
items: [{{ location.pk }}],
|
||||
key: 'location',
|
||||
url: '{% url "api-stocklocation-report-list" %}',
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if location %}
|
||||
$("#barcode-scan-in-items").click(function() {
|
||||
barcodeCheckInStockItems({{ location.id }});
|
||||
|
@ -115,6 +115,7 @@ class RuleSet(models.Model):
|
||||
'stock_location': [
|
||||
'stock_stocklocation',
|
||||
'label_stocklocationlabel',
|
||||
'report_stocklocationreport'
|
||||
],
|
||||
'stock': [
|
||||
'stock_stockitem',
|
||||
|
Loading…
Reference in New Issue
Block a user