mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +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>
|
<hr>
|
||||||
<p>{{ order.description }}</p>
|
<p>{{ order.description }}</p>
|
||||||
<div class='btn-row'>
|
<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 %}
|
{% if roles.purchase_order.change %}
|
||||||
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
<span class='fas fa-edit icon-green'></span>
|
||||||
@ -156,6 +159,10 @@ $("#place-order").click(function() {
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
$('#print-order-report').click(function() {
|
||||||
|
printPurchaseOrderReports([{{ order.pk }}]);
|
||||||
|
});
|
||||||
|
|
||||||
$("#edit-order").click(function() {
|
$("#edit-order").click(function() {
|
||||||
launchModalForm("{% url 'po-edit' order.id %}",
|
launchModalForm("{% url 'po-edit' order.id %}",
|
||||||
{
|
{
|
||||||
|
@ -15,10 +15,15 @@ InvenTree | {% trans "Purchase Orders" %}
|
|||||||
|
|
||||||
<div id='table-buttons'>
|
<div id='table-buttons'>
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
|
<div class='btn-group'>
|
||||||
{% if roles.purchase_order.add %}
|
{% if roles.purchase_order.add %}
|
||||||
<button class='btn btn-primary' type='button' id='po-create' title='{% trans "Create new purchase order" %}'>
|
<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>
|
<span class='fas fa-plus-circle'></span> {% trans "New Purchase Order" %}
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% 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" %}'>
|
<button class='btn btn-default' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
||||||
<span class='fas fa-calendar-alt'></span>
|
<span class='fas fa-calendar-alt'></span>
|
||||||
</button>
|
</button>
|
||||||
@ -29,6 +34,7 @@ InvenTree | {% trans "Purchase Orders" %}
|
|||||||
<!-- An empty div in which the filter list will be constructed -->
|
<!-- An empty div in which the filter list will be constructed -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed po-table' data-toolbar='#table-buttons' id='purchase-order-table'>
|
<table class='table table-striped table-condensed po-table' data-toolbar='#table-buttons' id='purchase-order-table'>
|
||||||
@ -154,6 +160,18 @@ $("#view-list").click(function() {
|
|||||||
$("#view-calendar").show();
|
$("#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() {
|
$("#po-create").click(function() {
|
||||||
launchModalForm("{% url 'po-create' %}",
|
launchModalForm("{% url 'po-create' %}",
|
||||||
{
|
{
|
||||||
|
@ -45,6 +45,9 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<p>{{ order.description }}</p>
|
<p>{{ order.description }}</p>
|
||||||
<div class='btn-row'>
|
<div class='btn-row'>
|
||||||
<div class='btn-group action-buttons'>
|
<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 %}
|
{% if roles.sales_order.change %}
|
||||||
<button type='button' class='btn btn-default' id='edit-order' title='Edit order information'>
|
<button type='button' class='btn btn-default' id='edit-order' title='Edit order information'>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
<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 %}
|
{% endblock %}
|
@ -15,10 +15,15 @@ InvenTree | {% trans "Sales Orders" %}
|
|||||||
|
|
||||||
<div id='table-buttons'>
|
<div id='table-buttons'>
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
|
<div class='btn-group'>
|
||||||
{% if roles.sales_order.add %}
|
{% if roles.sales_order.add %}
|
||||||
<button class='btn btn-primary' type='button' id='so-create' title='{% trans "Create new sales order" %}'>
|
<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>
|
<span class='fas fa-plus-circle'></span> {% trans "New Sales Order" %}
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% 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" %}'>
|
<button class='btn btn-default' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
||||||
<span class='fas fa-calendar-alt'></span>
|
<span class='fas fa-calendar-alt'></span>
|
||||||
</button>
|
</button>
|
||||||
@ -29,6 +34,7 @@ InvenTree | {% trans "Sales Orders" %}
|
|||||||
<!-- An empty div in which the filter list will be constructed -->
|
<!-- An empty div in which the filter list will be constructed -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed po-table' data-toolbar='#table-buttons' id='sales-order-table'>
|
<table class='table table-striped table-condensed po-table' data-toolbar='#table-buttons' id='sales-order-table'>
|
||||||
@ -156,10 +162,30 @@ loadSalesOrderTable("#sales-order-table", {
|
|||||||
url: "{% url 'api-so-list' %}",
|
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() {
|
$("#so-create").click(function() {
|
||||||
launchModalForm("{% url 'so-create' %}",
|
launchModalForm("{% url 'so-create' %}",
|
||||||
{
|
{
|
||||||
follow: true,
|
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 TestReport
|
||||||
from .models import BuildReport
|
from .models import BuildReport
|
||||||
from .models import BillOfMaterialsReport
|
from .models import BillOfMaterialsReport
|
||||||
|
from .models import PurchaseOrderReport
|
||||||
|
from .models import SalesOrderReport
|
||||||
|
|
||||||
|
|
||||||
class ReportTemplateAdmin(admin.ModelAdmin):
|
class ReportTemplateAdmin(admin.ModelAdmin):
|
||||||
@ -30,3 +32,5 @@ admin.site.register(ReportAsset, ReportAssetAdmin)
|
|||||||
admin.site.register(TestReport, ReportTemplateAdmin)
|
admin.site.register(TestReport, ReportTemplateAdmin)
|
||||||
admin.site.register(BuildReport, ReportTemplateAdmin)
|
admin.site.register(BuildReport, ReportTemplateAdmin)
|
||||||
admin.site.register(BillOfMaterialsReport, 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 build.models
|
||||||
import part.models
|
import part.models
|
||||||
|
import order.models
|
||||||
|
|
||||||
from .models import TestReport
|
from .models import TestReport
|
||||||
from .models import BuildReport
|
from .models import BuildReport
|
||||||
from .models import BillOfMaterialsReport
|
from .models import BillOfMaterialsReport
|
||||||
|
from .models import PurchaseOrderReport
|
||||||
|
from .models import SalesOrderReport
|
||||||
|
|
||||||
from .serializers import TestReportSerializer
|
from .serializers import TestReportSerializer
|
||||||
from .serializers import BuildReportSerializer
|
from .serializers import BuildReportSerializer
|
||||||
from .serializers import BOMReportSerializer
|
from .serializers import BOMReportSerializer
|
||||||
|
from .serializers import POReportSerializer
|
||||||
|
from .serializers import SOReportSerializer
|
||||||
|
|
||||||
|
|
||||||
class ReportListView(generics.ListAPIView):
|
class ReportListView(generics.ListAPIView):
|
||||||
@ -113,6 +118,40 @@ class BuildReportMixin:
|
|||||||
return build.models.Build.objects.filter(pk__in=valid_ids)
|
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:
|
class PartReportMixin:
|
||||||
"""
|
"""
|
||||||
Mixin for extracting part items from query params
|
Mixin for extracting part items from query params
|
||||||
@ -481,14 +520,203 @@ class BuildReportPrint(generics.RetrieveAPIView, BuildReportMixin, ReportPrintMi
|
|||||||
return self.print(request, builds)
|
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 = [
|
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
|
# Build reports
|
||||||
url(r'build/', include([
|
url(r'build/', include([
|
||||||
# Detail views
|
# Detail views
|
||||||
url(r'^(?P<pk>\d+)/', include([
|
url(r'^(?P<pk>\d+)/', include([
|
||||||
url(r'print/?', BuildReportPrint.as_view(), name='api-build-report-print'),
|
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
|
# 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 common.models
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
|
import order.models
|
||||||
|
|
||||||
from InvenTree.helpers import validateFilterString
|
from InvenTree.helpers import validateFilterString
|
||||||
|
|
||||||
@ -94,6 +95,22 @@ def validate_build_report_filters(filters):
|
|||||||
return validateFilterString(filters, model=build.models.Build)
|
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 WeasyprintReportMixin(WeasyTemplateResponseMixin):
|
||||||
"""
|
"""
|
||||||
Class for rendering a HTML template to a PDF.
|
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):
|
def rename_snippet(instance, filename):
|
||||||
|
|
||||||
filename = os.path.basename(filename)
|
filename = os.path.basename(filename)
|
||||||
|
@ -7,6 +7,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
|||||||
from .models import TestReport
|
from .models import TestReport
|
||||||
from .models import BuildReport
|
from .models import BuildReport
|
||||||
from .models import BillOfMaterialsReport
|
from .models import BillOfMaterialsReport
|
||||||
|
from .models import PurchaseOrderReport, SalesOrderReport
|
||||||
|
|
||||||
|
|
||||||
class TestReportSerializer(InvenTreeModelSerializer):
|
class TestReportSerializer(InvenTreeModelSerializer):
|
||||||
@ -55,3 +56,35 @@ class BOMReportSerializer(InvenTreeModelSerializer):
|
|||||||
'filters',
|
'filters',
|
||||||
'enabled',
|
'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 }}";
|
content: "v{{report_revision}} - {{ date.isoformat }}";
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block bottom_center %}
|
|
||||||
content: "www.currawong.aero";
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header_content %}
|
{% 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'>
|
<div class='header-right'>
|
||||||
<h3>
|
<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.conf import settings
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from company.models import Company
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
@ -72,6 +73,39 @@ def part_image(part):
|
|||||||
return f"file://{path}"
|
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()
|
@register.simple_tag()
|
||||||
def internal_link(link, text):
|
def internal_link(link, text):
|
||||||
"""
|
"""
|
||||||
|
@ -138,9 +138,9 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
formatNoMatches: function() { return '{% trans "No purchase orders found" %}'; },
|
formatNoMatches: function() { return '{% trans "No purchase orders found" %}'; },
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
title: '',
|
||||||
title: 'ID',
|
visible: true,
|
||||||
visible: false,
|
checkbox: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -234,9 +234,9 @@ function loadSalesOrderTable(table, options) {
|
|||||||
formatNoMatches: function() { return '{% trans "No sales orders found" %}'; },
|
formatNoMatches: function() { return '{% trans "No sales orders found" %}'; },
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
title: '',
|
||||||
title: 'ID',
|
checkbox: true,
|
||||||
visible: false,
|
visible: true,
|
||||||
switchable: false,
|
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_reportasset',
|
||||||
'report_reportsnippet',
|
'report_reportsnippet',
|
||||||
'report_billofmaterialsreport',
|
'report_billofmaterialsreport',
|
||||||
|
'report_purchaseorderreport',
|
||||||
|
'report_salesorderreport',
|
||||||
'users_owner',
|
'users_owner',
|
||||||
|
|
||||||
# Third-party tables
|
# Third-party tables
|
||||||
|
Loading…
x
Reference in New Issue
Block a user