@@ -156,10 +162,30 @@ loadSalesOrderTable("#sales-order-table", {
url: "{% url 'api-so-list' %}",
});
+$("#order-print").click(function() {
+ var rows = $("#sales-order-table").bootstrapTable('getSelections');
+
+ var orders = [];
+
+ rows.forEach(function(row) {
+ orders.push(row.pk);
+ });
+
+ printSalesOrderReports(orders);
+})
+
$("#so-create").click(function() {
launchModalForm("{% url 'so-create' %}",
{
follow: true,
+ secondary: [
+ {
+ field: 'customer',
+ label: '{% trans "New Customer" %}',
+ title: '{% trans "Create new Customer" %}',
+ url: '{% url "customer-create" %}',
+ }
+ ]
}
);
});
diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py
index 2c008877cc..b76b8fab1b 100644
--- a/InvenTree/report/admin.py
+++ b/InvenTree/report/admin.py
@@ -7,6 +7,8 @@ from .models import ReportSnippet, ReportAsset
from .models import TestReport
from .models import BuildReport
from .models import BillOfMaterialsReport
+from .models import PurchaseOrderReport
+from .models import SalesOrderReport
class ReportTemplateAdmin(admin.ModelAdmin):
@@ -30,3 +32,5 @@ admin.site.register(ReportAsset, ReportAssetAdmin)
admin.site.register(TestReport, ReportTemplateAdmin)
admin.site.register(BuildReport, ReportTemplateAdmin)
admin.site.register(BillOfMaterialsReport, ReportTemplateAdmin)
+admin.site.register(PurchaseOrderReport, ReportTemplateAdmin)
+admin.site.register(SalesOrderReport, ReportTemplateAdmin)
diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py
index 313d4de88d..212c57bd7f 100644
--- a/InvenTree/report/api.py
+++ b/InvenTree/report/api.py
@@ -18,14 +18,19 @@ from stock.models import StockItem
import build.models
import part.models
+import order.models
from .models import TestReport
from .models import BuildReport
from .models import BillOfMaterialsReport
+from .models import PurchaseOrderReport
+from .models import SalesOrderReport
from .serializers import TestReportSerializer
from .serializers import BuildReportSerializer
from .serializers import BOMReportSerializer
+from .serializers import POReportSerializer
+from .serializers import SOReportSerializer
class ReportListView(generics.ListAPIView):
@@ -113,6 +118,40 @@ class BuildReportMixin:
return build.models.Build.objects.filter(pk__in=valid_ids)
+class OrderReportMixin:
+ """
+ Mixin for extracting order items from query params
+
+ requires the OrderModel class attribute to be set!
+ """
+
+ def get_orders(self):
+ """
+ Return a list of order objects
+ """
+
+ orders = []
+
+ params = self.request.query_params
+
+ for key in ['order', 'order[]', 'orders', 'orders[]']:
+ if key in params:
+ orders = params.getlist(key, [])
+ break
+
+ valid_ids = []
+
+ for o in orders:
+ try:
+ valid_ids.append(int(o))
+ except (ValueError):
+ pass
+
+ valid_orders = self.OrderModel.objects.filter(pk__in=valid_ids)
+
+ return valid_orders
+
+
class PartReportMixin:
"""
Mixin for extracting part items from query params
@@ -481,14 +520,203 @@ class BuildReportPrint(generics.RetrieveAPIView, BuildReportMixin, ReportPrintMi
return self.print(request, builds)
+class POReportList(ReportListView, OrderReportMixin):
+
+ OrderModel = order.models.PurchaseOrder
+
+ queryset = PurchaseOrderReport.objects.all()
+ serializer_class = POReportSerializer
+
+ def filter_queryset(self, queryset):
+
+ queryset = super().filter_queryset(queryset)
+
+ orders = self.get_orders()
+
+ if len(orders) > 0:
+ """
+ We wish to filter by purchase orders
+
+ We need to compare the 'filters' string of each report,
+ and see if it matches against each of the specified orders.
+
+ TODO: In the future, perhaps there is a way to make this more efficient.
+ """
+
+ valid_report_ids = set()
+
+ for report in queryset.all():
+
+ matches = True
+
+ # Filter string defined for the report object
+ try:
+ filters = InvenTree.helpers.validateFilterString(report.filters)
+ except:
+ continue
+
+ for o in orders:
+ order_query = order.models.PurchaseOrder.objects.filter(pk=o.pk)
+
+ try:
+ if not order_query.filter(**filters).exists():
+ matches = False
+ break
+ except FieldError:
+ matches = False
+ break
+
+ if matches:
+ valid_report_ids.add(report.pk)
+ else:
+ continue
+
+ # Reduce queryset to only valid matches
+ queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids])
+
+ return queryset
+
+
+class POReportDetail(generics.RetrieveUpdateDestroyAPIView):
+ """
+ API endpoint for a single PurchaseOrderReport object
+ """
+
+ queryset = PurchaseOrderReport.objects.all()
+ serializer_class = POReportSerializer
+
+
+class POReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
+ """
+ API endpoint for printing a PurchaseOrderReport object
+ """
+
+ OrderModel = order.models.PurchaseOrder
+
+ queryset = PurchaseOrderReport.objects.all()
+ serializer_class = POReportSerializer
+
+ def get(self, request, *args, **kwargs):
+
+ orders = self.get_orders()
+
+ return self.print(request, orders)
+
+
+class SOReportList(ReportListView, OrderReportMixin):
+
+ OrderModel = order.models.SalesOrder
+
+ queryset = SalesOrderReport.objects.all()
+ serializer_class = SOReportSerializer
+
+ def filter_queryset(self, queryset):
+
+ queryset = super().filter_queryset(queryset)
+
+ orders = self.get_orders()
+
+ if len(orders) > 0:
+ """
+ We wish to filter by purchase orders
+
+ We need to compare the 'filters' string of each report,
+ and see if it matches against each of the specified orders.
+
+ TODO: In the future, perhaps there is a way to make this more efficient.
+ """
+
+ valid_report_ids = set()
+
+ for report in queryset.all():
+
+ matches = True
+
+ # Filter string defined for the report object
+ try:
+ filters = InvenTree.helpers.validateFilterString(report.filters)
+ except:
+ continue
+
+ for o in orders:
+ order_query = order.models.SalesOrder.objects.filter(pk=o.pk)
+
+ try:
+ if not order_query.filter(**filters).exists():
+ matches = False
+ break
+ except FieldError:
+ matches = False
+ break
+
+ if matches:
+ valid_report_ids.add(report.pk)
+ else:
+ continue
+
+ # Reduce queryset to only valid matches
+ queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids])
+
+ return queryset
+
+
+class SOReportDetail(generics.RetrieveUpdateDestroyAPIView):
+ """
+ API endpoint for a single SalesOrderReport object
+ """
+
+ queryset = SalesOrderReport.objects.all()
+ serializer_class = SOReportSerializer
+
+
+class SOReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
+ """
+ API endpoint for printing a PurchaseOrderReport object
+ """
+
+ OrderModel = order.models.SalesOrder
+
+ queryset = SalesOrderReport.objects.all()
+ serializer_class = SOReportSerializer
+
+ def get(self, request, *args, **kwargs):
+
+ orders = self.get_orders()
+
+ return self.print(request, orders)
+
+
report_api_urls = [
+ # Purchase order reports
+ url(r'po/', include([
+ # Detail views
+ url(r'^(?P
\d+)/', include([
+ url(r'print/', POReportPrint.as_view(), name='api-po-report-print'),
+ url(r'^$', POReportDetail.as_view(), name='api-po-report-detail'),
+ ])),
+
+ # List view
+ url(r'^$', POReportList.as_view(), name='api-po-report-list'),
+ ])),
+
+ # Sales order reports
+ url(r'so/', include([
+ # Detail views
+ url(r'^(?P\d+)/', include([
+ url(r'print/', SOReportPrint.as_view(), name='api-so-report-print'),
+ url(r'^$', SOReportDetail.as_view(), name='api-so-report-detail'),
+ ])),
+
+ url(r'^$', SOReportList.as_view(), name='api-so-report-list'),
+ ])),
+
# Build reports
url(r'build/', include([
# Detail views
url(r'^(?P\d+)/', include([
url(r'print/?', BuildReportPrint.as_view(), name='api-build-report-print'),
- url(r'^.*$', BuildReportDetail.as_view(), name='api-build-report-detail'),
+ url(r'^.$', BuildReportDetail.as_view(), name='api-build-report-detail'),
])),
# List view
diff --git a/InvenTree/report/migrations/0014_purchaseorderreport_salesorderreport.py b/InvenTree/report/migrations/0014_purchaseorderreport_salesorderreport.py
new file mode 100644
index 0000000000..ab734b7b48
--- /dev/null
+++ b/InvenTree/report/migrations/0014_purchaseorderreport_salesorderreport.py
@@ -0,0 +1,45 @@
+# Generated by Django 3.0.7 on 2021-03-10 05:46
+
+import django.core.validators
+from django.db import migrations, models
+import report.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('report', '0013_testreport_include_installed'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PurchaseOrderReport',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('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')),
+ ('enabled', models.BooleanField(default=True, help_text='Report template is enabled', verbose_name='Enabled')),
+ ('filters', models.CharField(blank=True, help_text='Purchase order query filters', max_length=250, validators=[report.models.validate_purchase_order_filters], verbose_name='Filters')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='SalesOrderReport',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('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')),
+ ('enabled', models.BooleanField(default=True, help_text='Report template is enabled', verbose_name='Enabled')),
+ ('filters', models.CharField(blank=True, help_text='Sales order query filters', max_length=250, validators=[report.models.validate_sales_order_filters], verbose_name='Filters')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py
index abbdea9025..a838396bf0 100644
--- a/InvenTree/report/models.py
+++ b/InvenTree/report/models.py
@@ -24,6 +24,7 @@ import build.models
import common.models
import part.models
import stock.models
+import order.models
from InvenTree.helpers import validateFilterString
@@ -94,6 +95,22 @@ def validate_build_report_filters(filters):
return validateFilterString(filters, model=build.models.Build)
+def validate_purchase_order_filters(filters):
+ """
+ Validate filter string against PurchaseOrder model
+ """
+
+ return validateFilterString(filters, model=order.models.PurchaseOrder)
+
+
+def validate_sales_order_filters(filters):
+ """
+ Validate filter string against SalesOrder model
+ """
+
+ return validateFilterString(filters, model=order.models.SalesOrder)
+
+
class WeasyprintReportMixin(WeasyTemplateResponseMixin):
"""
Class for rendering a HTML template to a PDF.
@@ -383,6 +400,74 @@ class BillOfMaterialsReport(ReportTemplateBase):
}
+class PurchaseOrderReport(ReportTemplateBase):
+ """
+ Render a report against a PurchaseOrder object
+ """
+
+ @classmethod
+ def getSubdir(cls):
+ return 'purchaseorder'
+
+ filters = models.CharField(
+ blank=True,
+ max_length=250,
+ verbose_name=_('Filters'),
+ help_text=_('Purchase order query filters'),
+ validators=[
+ validate_purchase_order_filters,
+ ]
+ )
+
+ def get_context_data(self, request):
+
+ order = self.object_to_print
+
+ return {
+ 'description': order.description,
+ 'lines': order.lines,
+ 'order': order,
+ 'reference': order.reference,
+ 'supplier': order.supplier,
+ 'prefix': common.models.InvenTreeSetting.get_setting('PURCHASEORDER_REFERENCE_PREFIX'),
+ 'title': str(order),
+ }
+
+
+class SalesOrderReport(ReportTemplateBase):
+ """
+ Render a report against a SalesOrder object
+ """
+
+ @classmethod
+ def getSubdir(cls):
+ return 'salesorder'
+
+ filters = models.CharField(
+ blank=True,
+ max_length=250,
+ verbose_name=_('Filters'),
+ help_text=_('Sales order query filters'),
+ validators=[
+ validate_sales_order_filters
+ ]
+ )
+
+ def get_context_data(self, request):
+
+ order = self.object_to_print
+
+ return {
+ 'customer': order.customer,
+ 'description': order.description,
+ 'lines': order.lines,
+ 'order': order,
+ 'prefix': common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_PREFIX'),
+ 'reference': order.reference,
+ 'title': str(order),
+ }
+
+
def rename_snippet(instance, filename):
filename = os.path.basename(filename)
diff --git a/InvenTree/report/serializers.py b/InvenTree/report/serializers.py
index f0a449ae49..fa7de1a3ea 100644
--- a/InvenTree/report/serializers.py
+++ b/InvenTree/report/serializers.py
@@ -7,6 +7,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField
from .models import TestReport
from .models import BuildReport
from .models import BillOfMaterialsReport
+from .models import PurchaseOrderReport, SalesOrderReport
class TestReportSerializer(InvenTreeModelSerializer):
@@ -55,3 +56,35 @@ class BOMReportSerializer(InvenTreeModelSerializer):
'filters',
'enabled',
]
+
+
+class POReportSerializer(InvenTreeModelSerializer):
+
+ template = InvenTreeAttachmentSerializerField(required=True)
+
+ class Meta:
+ model = PurchaseOrderReport
+ fields = [
+ 'pk',
+ 'name',
+ 'description',
+ 'template',
+ 'filters',
+ 'enabled',
+ ]
+
+
+class SOReportSerializer(InvenTreeModelSerializer):
+
+ template = InvenTreeAttachmentSerializerField(required=True)
+
+ class Meta:
+ model = SalesOrderReport
+ fields = [
+ 'pk',
+ 'name',
+ 'description',
+ 'template',
+ 'filters',
+ 'enabled',
+ ]
diff --git a/InvenTree/report/templates/report/inventree_build_order_base.html b/InvenTree/report/templates/report/inventree_build_order_base.html
index 46e7544df5..6c8af84f1d 100644
--- a/InvenTree/report/templates/report/inventree_build_order_base.html
+++ b/InvenTree/report/templates/report/inventree_build_order_base.html
@@ -77,12 +77,9 @@ margin-top: 4cm;
content: "v{{report_revision}} - {{ date.isoformat }}";
{% endblock %}
-{% block bottom_center %}
-content: "www.currawong.aero";
-{% endblock %}
-
{% block header_content %}
-
+
+