mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1397 from SchrodingersGat/order-report
Order report
This commit is contained in:
commit
ed028aed62
@ -35,7 +35,10 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<hr>
|
||||
<p>{{ order.description }}</p>
|
||||
<div class='btn-row'>
|
||||
<div class='btn-group action-buttons'>
|
||||
<div class='btn-group action-buttons' role='group'>
|
||||
<button type='button' class='btn btn-default' id='print-order-report' title='{% trans "Print" %}'>
|
||||
<span class='fas fa-print'></span>
|
||||
</button>
|
||||
{% if roles.purchase_order.change %}
|
||||
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
||||
<span class='fas fa-edit icon-green'></span>
|
||||
@ -156,6 +159,10 @@ $("#place-order").click(function() {
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
$('#print-order-report').click(function() {
|
||||
printPurchaseOrderReports([{{ order.pk }}]);
|
||||
});
|
||||
|
||||
$("#edit-order").click(function() {
|
||||
launchModalForm("{% url 'po-edit' order.id %}",
|
||||
{
|
||||
|
@ -15,18 +15,24 @@ InvenTree | {% trans "Purchase Orders" %}
|
||||
|
||||
<div id='table-buttons'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<button class='btn btn-primary' type='button' id='po-create' title='{% trans "Create new purchase order" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Purchase Order" %}</button>
|
||||
{% endif %}
|
||||
<button class='btn btn-default' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
||||
<span class='fas fa-calendar-alt'></span>
|
||||
</button>
|
||||
<button class='btn btn-default' 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-purchaseorder'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
<div class='btn-group'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<button class='btn btn-primary' type='button' id='po-create' title='{% trans "Create new purchase order" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Purchase Order" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button id='order-print' class='btn btn-default' title='{% trans "Print Order Reports" %}'>
|
||||
<span class='fas fa-print'></span>
|
||||
</button>
|
||||
<button class='btn btn-default' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
||||
<span class='fas fa-calendar-alt'></span>
|
||||
</button>
|
||||
<button class='btn btn-default' 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-purchaseorder'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -154,6 +160,18 @@ $("#view-list").click(function() {
|
||||
$("#view-calendar").show();
|
||||
});
|
||||
|
||||
$("#order-print").click(function() {
|
||||
var rows = $("#purchase-order-table").bootstrapTable('getSelections');
|
||||
|
||||
var orders = [];
|
||||
|
||||
rows.forEach(function(row) {
|
||||
orders.push(row.pk);
|
||||
});
|
||||
|
||||
printPurchaseOrderReports(orders);
|
||||
})
|
||||
|
||||
$("#po-create").click(function() {
|
||||
launchModalForm("{% url 'po-create' %}",
|
||||
{
|
||||
|
@ -45,6 +45,9 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<p>{{ order.description }}</p>
|
||||
<div class='btn-row'>
|
||||
<div class='btn-group action-buttons'>
|
||||
<button type='button' class='btn btn-default' id='print-order-report' title='{% trans "Print" %}'>
|
||||
<span class='fas fa-print'></span>
|
||||
</button>
|
||||
{% if roles.sales_order.change %}
|
||||
<button type='button' class='btn btn-default' id='edit-order' title='Edit order information'>
|
||||
<span class='fas fa-edit icon-green'></span>
|
||||
@ -165,4 +168,8 @@ $("#ship-order").click(function() {
|
||||
});
|
||||
});
|
||||
|
||||
$('#print-order-report').click(function() {
|
||||
printSalesOrderReports([{{ order.pk }}]);
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -15,18 +15,24 @@ InvenTree | {% trans "Sales Orders" %}
|
||||
|
||||
<div id='table-buttons'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
{% if roles.sales_order.add %}
|
||||
<button class='btn btn-primary' type='button' id='so-create' title='{% trans "Create new sales order" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Sales Order" %}</button>
|
||||
{% endif %}
|
||||
<button class='btn btn-default' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
||||
<span class='fas fa-calendar-alt'></span>
|
||||
</button>
|
||||
<button class='btn btn-default' 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-salesorder'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
<div class='btn-group'>
|
||||
{% if roles.sales_order.add %}
|
||||
<button class='btn btn-primary' type='button' id='so-create' title='{% trans "Create new sales order" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Sales Order" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button id='order-print' class='btn btn-default' title='{% trans "Print Order Reports" %}'>
|
||||
<span class='fas fa-print'></span>
|
||||
</button>
|
||||
<button class='btn btn-default' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
||||
<span class='fas fa-calendar-alt'></span>
|
||||
</button>
|
||||
<button class='btn btn-default' 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-salesorder'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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" %}',
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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)
|
||||
|
@ -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<pk>\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<pk>\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<pk>\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
|
||||
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -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 %}
|
||||
<img class='logo' src="{% asset 'logo_black_with_black_bird.png' %}" alt="hello" width="150">
|
||||
<!-- TODO - Make the company logo asset generic -->
|
||||
<img class='logo' src="{% asset 'company_logo.png' %}" alt="hello" width="150">
|
||||
|
||||
<div class='header-right'>
|
||||
<h3>
|
||||
|
116
InvenTree/report/templates/report/inventree_po_report.html
Normal file
116
InvenTree/report/templates/report/inventree_po_report.html
Normal file
@ -0,0 +1,116 @@
|
||||
{% extends "report/inventree_report_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load report %}
|
||||
{% load barcode %}
|
||||
{% load inventree_extras %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
margin-top: 4cm;
|
||||
{% endblock %}
|
||||
|
||||
{% block bottom_left %}
|
||||
content: "v{{report_revision}} - {{ date.isoformat }}";
|
||||
{% endblock %}
|
||||
|
||||
{% block bottom_center %}
|
||||
content: "InvenTree v{% inventree_version %}";
|
||||
{% endblock %}
|
||||
|
||||
{% 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;
|
||||
}
|
||||
|
||||
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%
|
||||
}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block header_content %}
|
||||
|
||||
<img class='logo' src='{% company_image supplier %}' alt="{{ supplier }}" width='150'>
|
||||
|
||||
<div class='header-right'>
|
||||
<h3>{% trans "Purchase Order" %} {{ prefix }}{{ reference }}</h3>
|
||||
{{ supplier.name }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<h3>{% trans "Line Items" %}</h3>
|
||||
|
||||
<table class='table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Part" %}</th>
|
||||
<th>{% trans "Quantity" %}</th>
|
||||
<th>{% trans "Reference" %}</th>
|
||||
<th>{% trans "Note" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in lines.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class='thumb-container'>
|
||||
<img src='{% part_image line.part.part %}' class='part-thumb'>
|
||||
</div>
|
||||
<div class='part-text'>
|
||||
{{ line.part.part.full_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>{% decimal line.quantity %}</td>
|
||||
<td>{{ line.reference }}</td>
|
||||
<td>{{ line.notes }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{% endblock %}
|
116
InvenTree/report/templates/report/inventree_so_report.html
Normal file
116
InvenTree/report/templates/report/inventree_so_report.html
Normal file
@ -0,0 +1,116 @@
|
||||
{% extends "report/inventree_report_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load report %}
|
||||
{% load barcode %}
|
||||
{% load inventree_extras %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
margin-top: 4cm;
|
||||
{% endblock %}
|
||||
|
||||
{% block bottom_left %}
|
||||
content: "v{{report_revision}} - {{ date.isoformat }}";
|
||||
{% endblock %}
|
||||
|
||||
{% block bottom_center %}
|
||||
content: "InvenTree v{% inventree_version %}";
|
||||
{% endblock %}
|
||||
|
||||
{% 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;
|
||||
}
|
||||
|
||||
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%
|
||||
}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block header_content %}
|
||||
|
||||
<img class='logo' src='{% company_image customer %}' alt="{{ customer }}" width='150'>
|
||||
|
||||
<div class='header-right'>
|
||||
<h3>{% trans "Sales Order" %} {{ prefix }}{{ reference }}</h3>
|
||||
{{ customer.name }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<h3>{% trans "Line Items" %}</h3>
|
||||
|
||||
<table class='table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Part" %}</th>
|
||||
<th>{% trans "Quantity" %}</th>
|
||||
<th>{% trans "Reference" %}</th>
|
||||
<th>{% trans "Note" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in lines.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class='thumb-container'>
|
||||
<img src='{% part_image line.part %}' class='part-thumb'>
|
||||
</div>
|
||||
<div class='part-text'>
|
||||
{{ line.part.full_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>{% decimal line.quantity %}</td>
|
||||
<td>{{ line.reference }}</td>
|
||||
<td>{{ line.notes }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{% endblock %}
|
@ -8,6 +8,7 @@ from django import template
|
||||
from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from company.models import Company
|
||||
from part.models import Part
|
||||
from stock.models import StockItem
|
||||
|
||||
@ -72,6 +73,39 @@ def part_image(part):
|
||||
return f"file://{path}"
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def company_image(company):
|
||||
"""
|
||||
Return a fully-qualified path for a company image
|
||||
"""
|
||||
|
||||
# If in debug mode, return the URL to the image, not a local file
|
||||
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
|
||||
|
||||
if type(company) is Company:
|
||||
img = company.image.name
|
||||
else:
|
||||
img = ''
|
||||
|
||||
if debug_mode:
|
||||
if img:
|
||||
return os.path.join(settings.MEDIA_URL, img)
|
||||
else:
|
||||
return os.path.join(settings.STATIC_URL, 'img', 'blank_image.png')
|
||||
|
||||
else:
|
||||
path = os.path.join(settings.MEDIA_ROOT, img)
|
||||
path = os.path.abspath(path)
|
||||
|
||||
if not os.path.exists(path) or not os.path.isfile(path):
|
||||
# Image does not exist
|
||||
# Return the 'blank' image
|
||||
path = os.path.join(settings.STATIC_ROOT, 'img', 'blank_image.png')
|
||||
path = os.path.abspath(path)
|
||||
|
||||
return f"file://{path}"
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def internal_link(link, text):
|
||||
"""
|
||||
|
@ -138,9 +138,9 @@ function loadPurchaseOrderTable(table, options) {
|
||||
formatNoMatches: function() { return '{% trans "No purchase orders found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false,
|
||||
title: '',
|
||||
visible: true,
|
||||
checkbox: true,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
@ -234,9 +234,9 @@ function loadSalesOrderTable(table, options) {
|
||||
formatNoMatches: function() { return '{% trans "No sales orders found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false,
|
||||
title: '',
|
||||
checkbox: true,
|
||||
visible: true,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
|
@ -247,3 +247,111 @@ function printBomReports(parts, options={}) {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function printPurchaseOrderReports(orders, options={}) {
|
||||
/**
|
||||
* Print PO reports for the provided purchase order(s)
|
||||
*/
|
||||
|
||||
if (orders.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Purchase Orders" %}',
|
||||
'{% trans "Purchase Order(s) must be selected before printing report" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Request avaiable report templates
|
||||
inventreeGet(
|
||||
'{% url "api-po-report-list" %}',
|
||||
{
|
||||
enabled: true,
|
||||
orders: orders,
|
||||
},
|
||||
{
|
||||
success: function(response) {
|
||||
if (response.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "No Reports Found" %}',
|
||||
'{% trans "No report templates found which match selected orders" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Select report template
|
||||
selectReport(
|
||||
response,
|
||||
orders,
|
||||
{
|
||||
success: function(pk) {
|
||||
var href = `/api/report/po/${pk}/print/?`;
|
||||
|
||||
orders.forEach(function(order) {
|
||||
href += `order=${order}&`;
|
||||
});
|
||||
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function printSalesOrderReports(orders, options={}) {
|
||||
/**
|
||||
* Print SO reports for the provided purchase order(s)
|
||||
*/
|
||||
|
||||
if (orders.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Sales Orders" %}',
|
||||
'{% trans "Sales Order(s) must be selected before printing report" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Request avaiable report templates
|
||||
inventreeGet(
|
||||
'{% url "api-so-report-list" %}',
|
||||
{
|
||||
enabled: true,
|
||||
orders: orders,
|
||||
},
|
||||
{
|
||||
success: function(response) {
|
||||
if (response.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "No Reports Found" %}',
|
||||
'{% trans "No report templates found which match selected orders" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Select report template
|
||||
selectReport(
|
||||
response,
|
||||
orders,
|
||||
{
|
||||
success: function(pk) {
|
||||
var href = `/api/report/so/${pk}/print/?`;
|
||||
|
||||
orders.forEach(function(order) {
|
||||
href += `order=${order}&`;
|
||||
});
|
||||
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -124,6 +124,8 @@ class RuleSet(models.Model):
|
||||
'report_reportasset',
|
||||
'report_reportsnippet',
|
||||
'report_billofmaterialsreport',
|
||||
'report_purchaseorderreport',
|
||||
'report_salesorderreport',
|
||||
'users_owner',
|
||||
|
||||
# Third-party tables
|
||||
|
Loading…
Reference in New Issue
Block a user