mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-03 04:00:57 +00:00
[Feature] Add RMA support (#4488)
* Adds ReturnOrder and ReturnOrderAttachment models
* Adds new 'role' specific for return orders
* Refactor total_price into a mixin
- Required for PurchaseOrder and SalesOrder
- May not be required for ReturnOrder (remains to be seen)
* Adds API endpoints for ReturnOrder
- Add list endpoint
- Add detail endpoint
- Adds required serializer models
* Adds basic "index" page for Return Order model
* Update API version
* Update navbar text
* Add db migration for new "role"
* Add ContactList and ContactDetail API endpoints
* Adds template and JS code for manipulation of contacts
- Display a table
- Create / edit / delete
* Splits order.js into multiple files
- Javascript files was becoming extremely large
- Hard to debug and find code
- Split into purchase_order / return_order / sales_order
* Fix role name (change 'returns' to 'return_order')
- Similar to existing roles for purchase_order and sales_order
* Adds detail page for ReturnOrder
* URL cleanup
- Use <int:pk> instead of complex regex
* More URL cleanup
* Add "return orders" list to company detail page
* Break JS status codes into new javascript file
- Always difficult to track down where these are rendered
- Enough to warrant their own file now
* Add ability to edit return order from detail page
* Database migrations
- Add new ReturnOrder modeles
- Add new 'contact' field to external orders
* Adds "contact" to ReturnOrder
- Implement check to ensure that the selected "contact" matches the selected "company"
* Adjust filters to limit contact options
* Fix typo
* Expose 'contact' field for PurchaseOrder model
* Render contact information
* Add "contact" for SalesOrder
* Adds setting to enable / disable return order functionality
- Simply hides the navigation elements
- API is not disabled
* Support filtering ReturnOrder by 'status'
- Refactors existing filter into the OrderFilter class
* js linting
* More JS linting
* Adds ReturnOrderReport model
* Add serializer for the ReturnOrderReport model
- A little bit of refactoring along the way
* Admin integration for new report model
* Refactoring for report.api
- Adds generic mixins for filtering queryset (based on updates to label.api)
- Reduces repeated code a *lot*
* Exposes API endpoints for ReturnOrderReport
* Adds default example report file for ReturnOrder
- Requires some more work :)
* Refactor report printing javascript code
- Replace all existing functions with 'printReports'
* Improvements for default StockItem test report template
- Fix bug in template
- Handle potential errors in template tags
- Add more helpers to report tags
- Improve test result rendering
* Reduce logging verbosity from weasyprint
* Refactor javascript for label printing
- Consolidate into a single function
- Similar to refactor of report functions
* Add report print button to return order page
* Record user reference when creating via API
* Refactor order serializers
- Move common code into AbstractOrderSerializer class
* Adds extra line item model for the return order
- Adds serializer and API endpoints as appropriate
* Render extra line table for return order
- Refactor existing functions into a single generic function
- Reduces repeated JS code a lot
* Add ability to create a new extra line item
* Adds button for creating a new lien item
* JS linting
* Update test
* Typo fix
(cherry picked from commit 28ac2be35b
)
* Enable search for return order
* Don't do pricing (yet) for returnorder extra line table
- Fixes an uncaught error
* Error catching for api.js
* Updates for order models:
- Add 'target_date' field to abstract Order model
- Add IN_PROGRESS status code for return order
- Refactor 'overdue' and 'outstanding' API queries
- Refactor OVERDUE_FILTER on order models
- Refactor is_overdue on order models
- More table filters for return order model
* JS cleanup
* Create ReturnOrderLineItem model
- New type of status label
- Add TotalPriceMixin to ReturnOrder model
* Adds an API serializer for the ReturnOrderLineItem model
* Add API endpoints for ReturnOrderLineItem model
- Including some refactoring along the way
* javascript: refactor loadTableFilters function
- Pass enforced query through to the filters
- Call Object.assign() to construct a superset query
- Removes a lot of code duplication
* Refactor hard-coded URLS to use {% url %} lookup
- Forces error if the URL is wrong
- If we ever change the URL, will still work
* Implement creation of new return order line items
* Adds 'part_detail' annotation to ReturnOrderLineItem serializer
- Required for rendering part information
* javascript: refactor method for creating a group of buttons in a table
* javascript: refactor common buttons with helper functions
* Allow edit and delete of return order line items
* Add form option to automatically reload a table on success
- Pass table name to options.refreshTable
* JS linting
* Add common function for createExtraLineItem
* Refactor loading of attachment tables
- Setup drag-and-drop as part of core function
* CI fixes
* Refactoring out some more common API endpoint code
* Update migrations
* Fix permission typo
* Refactor for unit testing code
* Add unit tests for Contact model
* Tests for returnorder list API
* Annotate 'line_items' to ReturnOrder serializer
* Driving the refactor tractor
* More unit tests for the ReturnOrder API endpoints
* Refactor "print orders" button for various order tables
- Move into "setupFilterList" code (generic)
* add generic 'label printing' button to table actions buttons
* Refactor build output table
* Refactoring icon generation for js
* Refactoring for Part API
* Fix database model type for 'received_date'
* Add API endpoint to "issue" a ReturnOrder
* Improvements for stock tracking table
- Add new status codes
- Add rendering for SalesOrder
- Add rendering for ReturnOrder
- Fix status badges
* Adds functionality to receive line items against a return order
* Add endpoints for completing and cancelling orders
* Add option to allow / prevent editing of ReturnOrder after completed
* js linting
* Wrap "add extra line" button in setting check
* Updates to order/admin.py
* Remove inline admin for returnorderline model
* Updates to pass CI
* Serializer fix
* order template fixes
* Unit test fix
* Fixes for ReturnOrder.receive_line_item
* Unit testing for receiving line items against an RMA
* Improve example report for return order
* Extend unit tests for reporting
* Cleanup here and there
* Unit testing for order views
* Clear "sales_order" field when returning against ReturnOrder
* Add 'location' to deltas when returning from customer
* Bug fix for unit test
This commit is contained in:
@ -3,7 +3,8 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
|
||||
ReportAsset, ReportSnippet, SalesOrderReport, TestReport)
|
||||
ReportAsset, ReportSnippet, ReturnOrderReport,
|
||||
SalesOrderReport, TestReport)
|
||||
|
||||
|
||||
class ReportTemplateAdmin(admin.ModelAdmin):
|
||||
@ -28,4 +29,5 @@ admin.site.register(TestReport, ReportTemplateAdmin)
|
||||
admin.site.register(BuildReport, ReportTemplateAdmin)
|
||||
admin.site.register(BillOfMaterialsReport, ReportTemplateAdmin)
|
||||
admin.site.register(PurchaseOrderReport, ReportTemplateAdmin)
|
||||
admin.site.register(ReturnOrderReport, ReportTemplateAdmin)
|
||||
admin.site.register(SalesOrderReport, ReportTemplateAdmin)
|
||||
|
@ -24,9 +24,10 @@ from plugin.serializers import MetadataSerializer
|
||||
from stock.models import StockItem, StockItemAttachment
|
||||
|
||||
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
|
||||
SalesOrderReport, TestReport)
|
||||
ReturnOrderReport, SalesOrderReport, TestReport)
|
||||
from .serializers import (BOMReportSerializer, BuildReportSerializer,
|
||||
PurchaseOrderReportSerializer,
|
||||
ReturnOrderReportSerializer,
|
||||
SalesOrderReportSerializer, TestReportSerializer)
|
||||
|
||||
|
||||
@ -423,6 +424,31 @@ class SalesOrderReportPrint(SalesOrderReportMixin, ReportPrintMixin, RetrieveAPI
|
||||
pass
|
||||
|
||||
|
||||
class ReturnOrderReportMixin(ReportFilterMixin):
|
||||
"""Mixin for the ReturnOrderReport report template"""
|
||||
|
||||
ITEM_MODEL = order.models.ReturnOrder
|
||||
ITEM_KEY = 'order'
|
||||
|
||||
queryset = ReturnOrderReport.objects.all()
|
||||
serializer_class = ReturnOrderReportSerializer
|
||||
|
||||
|
||||
class ReturnOrderReportList(ReturnOrderReportMixin, ReportListView):
|
||||
"""API list endpoint for the ReturnOrderReport model"""
|
||||
pass
|
||||
|
||||
|
||||
class ReturnOrderReportDetail(ReturnOrderReportMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for a single ReturnOrderReport object"""
|
||||
pass
|
||||
|
||||
|
||||
class ReturnOrderReportPrint(ReturnOrderReportMixin, ReportPrintMixin, RetrieveAPI):
|
||||
"""API endpoint for printing a ReturnOrderReport object"""
|
||||
pass
|
||||
|
||||
|
||||
class ReportMetadata(RetrieveUpdateAPI):
|
||||
"""API endpoint for viewing / updating Report metadata."""
|
||||
MODEL_REF = 'reportmodel'
|
||||
@ -453,7 +479,7 @@ report_api_urls = [
|
||||
# Purchase order reports
|
||||
re_path(r'po/', include([
|
||||
# Detail views
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'print/', PurchaseOrderReportPrint.as_view(), name='api-po-report-print'),
|
||||
re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: PurchaseOrderReport}, name='api-po-report-metadata'),
|
||||
path('', PurchaseOrderReportDetail.as_view(), name='api-po-report-detail'),
|
||||
@ -466,7 +492,7 @@ report_api_urls = [
|
||||
# Sales order reports
|
||||
re_path(r'so/', include([
|
||||
# Detail views
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'print/', SalesOrderReportPrint.as_view(), name='api-so-report-print'),
|
||||
re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: SalesOrderReport}, name='api-so-report-metadata'),
|
||||
path('', SalesOrderReportDetail.as_view(), name='api-so-report-detail'),
|
||||
@ -475,10 +501,19 @@ report_api_urls = [
|
||||
path('', SalesOrderReportList.as_view(), name='api-so-report-list'),
|
||||
])),
|
||||
|
||||
# Return order reports
|
||||
re_path(r'return-order/', include([
|
||||
path(r'<int:pk>/', include([
|
||||
path(r'print/', ReturnOrderReportPrint.as_view(), name='api-return-order-report-print'),
|
||||
path('', ReturnOrderReportDetail.as_view(), name='api-return-order-report-detail'),
|
||||
])),
|
||||
path('', ReturnOrderReportList.as_view(), name='api-return-order-report-list'),
|
||||
])),
|
||||
|
||||
# Build reports
|
||||
re_path(r'build/', include([
|
||||
# Detail views
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'print/?', BuildReportPrint.as_view(), name='api-build-report-print'),
|
||||
re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: BuildReport}, name='api-build-report-metadata'),
|
||||
re_path(r'^.$', BuildReportDetail.as_view(), name='api-build-report-detail'),
|
||||
@ -492,7 +527,7 @@ report_api_urls = [
|
||||
re_path(r'bom/', include([
|
||||
|
||||
# Detail views
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'print/?', BOMReportPrint.as_view(), name='api-bom-report-print'),
|
||||
re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: BillOfMaterialsReport}, name='api-bom-report-metadata'),
|
||||
re_path(r'^.*$', BOMReportDetail.as_view(), name='api-bom-report-detail'),
|
||||
@ -505,7 +540,7 @@ report_api_urls = [
|
||||
# Stock item test reports
|
||||
re_path(r'test/', include([
|
||||
# Detail views
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'print/?', StockItemTestReportPrint.as_view(), name='api-stockitem-testreport-print'),
|
||||
re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: TestReport}, name='api-stockitem-testreport-metadata'),
|
||||
re_path(r'^.*$', StockItemTestReportDetail.as_view(), name='api-stockitem-testreport-detail'),
|
||||
|
@ -8,8 +8,6 @@ from pathlib import Path
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
from InvenTree.ready import canAppAccessDatabase
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
@ -19,12 +17,21 @@ class ReportConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
"""This function is called whenever the report app is loaded."""
|
||||
|
||||
from InvenTree.ready import canAppAccessDatabase
|
||||
|
||||
# Configure logging for PDF generation (disable "info" messages)
|
||||
logging.getLogger('fontTools').setLevel(logging.WARNING)
|
||||
logging.getLogger('weasyprint').setLevel(logging.WARNING)
|
||||
|
||||
# Create entries for default report templates
|
||||
if canAppAccessDatabase(allow_test=True):
|
||||
self.create_default_test_reports()
|
||||
self.create_default_build_reports()
|
||||
self.create_default_bill_of_materials_reports()
|
||||
self.create_default_purchase_order_reports()
|
||||
self.create_default_sales_order_reports()
|
||||
self.create_default_return_order_reports()
|
||||
|
||||
def create_default_reports(self, model, reports):
|
||||
"""Copy defualt report files across to the media directory."""
|
||||
@ -174,3 +181,23 @@ class ReportConfig(AppConfig):
|
||||
]
|
||||
|
||||
self.create_default_reports(SalesOrderReport, reports)
|
||||
|
||||
def create_default_return_order_reports(self):
|
||||
"""Create database entries for the default ReturnOrderReport templates"""
|
||||
|
||||
try:
|
||||
from report.models import ReturnOrderReport
|
||||
except Exception: # pragma: no cover
|
||||
# Database not yet ready
|
||||
return
|
||||
|
||||
# List of templates to copy across
|
||||
reports = [
|
||||
{
|
||||
'file': 'inventree_return_order_report.html',
|
||||
'name': 'InvenTree Return Order',
|
||||
'description': 'Return Order example report',
|
||||
}
|
||||
]
|
||||
|
||||
self.create_default_reports(ReturnOrderReport, reports)
|
||||
|
31
InvenTree/report/migrations/0018_returnorderreport.py
Normal file
31
InvenTree/report/migrations/0018_returnorderreport.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.2.18 on 2023-03-15 11:17
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import report.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('report', '0017_auto_20230317_0816'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ReturnOrderReport',
|
||||
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')),
|
||||
('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='Return order query filters', max_length=250, validators=[report.models.validate_return_order_filters], verbose_name='Filters')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.18 on 2023-03-23 11:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('report', '0018_returnorderreport'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='returnorderreport',
|
||||
name='metadata',
|
||||
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
|
||||
),
|
||||
]
|
@ -68,6 +68,11 @@ def validate_sales_order_filters(filters):
|
||||
return validateFilterString(filters, model=order.models.SalesOrder)
|
||||
|
||||
|
||||
def validate_return_order_filters(filters):
|
||||
"""Validate filter string against ReturnOrder model"""
|
||||
return validateFilterString(filters, model=order.models.ReturnOrder)
|
||||
|
||||
|
||||
class WeasyprintReportMixin(WeasyTemplateResponseMixin):
|
||||
"""Class for rendering a HTML template to a PDF."""
|
||||
|
||||
@ -303,6 +308,30 @@ class TestReport(ReportTemplateBase):
|
||||
|
||||
return items.exists()
|
||||
|
||||
def get_test_keys(self, stock_item):
|
||||
"""Construct a flattened list of test 'keys' for this StockItem:
|
||||
|
||||
- First, any 'required' tests
|
||||
- Second, any 'non required' tests
|
||||
- Finally, any test results which do not match a test
|
||||
"""
|
||||
|
||||
keys = []
|
||||
|
||||
for test in stock_item.part.getTestTemplates(required=True):
|
||||
if test.key not in keys:
|
||||
keys.append(test.key)
|
||||
|
||||
for test in stock_item.part.getTestTemplates(required=False):
|
||||
if test.key not in keys:
|
||||
keys.append(test.key)
|
||||
|
||||
for result in stock_item.testResultList(include_installed=self.include_installed):
|
||||
if result.key not in keys:
|
||||
keys.append(result.key)
|
||||
|
||||
return list(keys)
|
||||
|
||||
def get_context_data(self, request):
|
||||
"""Return custom context data for the TestReport template"""
|
||||
stock_item = self.object_to_print
|
||||
@ -312,6 +341,9 @@ class TestReport(ReportTemplateBase):
|
||||
'serial': stock_item.serial,
|
||||
'part': stock_item.part,
|
||||
'parameters': stock_item.part.parameters_map(),
|
||||
'test_keys': self.get_test_keys(stock_item),
|
||||
'test_template_list': stock_item.part.getTestTemplates(),
|
||||
'test_template_map': stock_item.part.getTestTemplateMap(),
|
||||
'results': stock_item.testResultMap(include_installed=self.include_installed),
|
||||
'result_list': stock_item.testResultList(include_installed=self.include_installed),
|
||||
'installed_items': stock_item.get_installed_items(cascade=True),
|
||||
@ -468,6 +500,45 @@ class SalesOrderReport(ReportTemplateBase):
|
||||
}
|
||||
|
||||
|
||||
class ReturnOrderReport(ReportTemplateBase):
|
||||
"""Render a custom report against a ReturnOrder object"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the ReturnOrderReport model"""
|
||||
return reverse('api-return-order-report-list')
|
||||
|
||||
@classmethod
|
||||
def getSubdir(cls):
|
||||
"""Return the directory where the ReturnOrderReport templates are stored"""
|
||||
return 'returnorder'
|
||||
|
||||
filters = models.CharField(
|
||||
blank=True,
|
||||
max_length=250,
|
||||
verbose_name=_('Filters'),
|
||||
help_text=_('Return order query filters'),
|
||||
validators=[
|
||||
validate_return_order_filters,
|
||||
]
|
||||
)
|
||||
|
||||
def get_context_data(self, request):
|
||||
"""Return custom context data for the ReturnOrderReport template"""
|
||||
|
||||
order = self.object_to_print
|
||||
|
||||
return {
|
||||
'order': order,
|
||||
'description': order.description,
|
||||
'reference': order.reference,
|
||||
'customer': order.customer,
|
||||
'lines': order.lines,
|
||||
'extra_lines': order.extra_lines,
|
||||
'title': str(order),
|
||||
}
|
||||
|
||||
|
||||
def rename_snippet(instance, filename):
|
||||
"""Function to rename a report snippet once uploaded"""
|
||||
|
||||
|
@ -4,99 +4,83 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
||||
InvenTreeModelSerializer)
|
||||
|
||||
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
|
||||
SalesOrderReport, TestReport)
|
||||
ReturnOrderReport, SalesOrderReport, TestReport)
|
||||
|
||||
|
||||
class TestReportSerializer(InvenTreeModelSerializer):
|
||||
class ReportSerializerBase(InvenTreeModelSerializer):
|
||||
"""Base class for report serializer"""
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
@staticmethod
|
||||
def report_fields():
|
||||
"""Generic serializer fields for a report template"""
|
||||
|
||||
return [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'template',
|
||||
'filters',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
|
||||
class TestReportSerializer(ReportSerializerBase):
|
||||
"""Serializer class for the TestReport model"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = TestReport
|
||||
fields = [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'template',
|
||||
'filters',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
fields = ReportSerializerBase.report_fields()
|
||||
|
||||
|
||||
class BuildReportSerializer(InvenTreeModelSerializer):
|
||||
class BuildReportSerializer(ReportSerializerBase):
|
||||
"""Serializer class for the BuildReport model"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = BuildReport
|
||||
fields = [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'template',
|
||||
'filters',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
fields = ReportSerializerBase.report_fields()
|
||||
|
||||
|
||||
class BOMReportSerializer(InvenTreeModelSerializer):
|
||||
class BOMReportSerializer(ReportSerializerBase):
|
||||
"""Serializer class for the BillOfMaterialsReport model"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = BillOfMaterialsReport
|
||||
fields = [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'template',
|
||||
'filters',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
fields = ReportSerializerBase.report_fields()
|
||||
|
||||
|
||||
class PurchaseOrderReportSerializer(InvenTreeModelSerializer):
|
||||
class PurchaseOrderReportSerializer(ReportSerializerBase):
|
||||
"""Serializer class for the PurchaseOrdeReport model"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = PurchaseOrderReport
|
||||
fields = [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'template',
|
||||
'filters',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
fields = ReportSerializerBase.report_fields()
|
||||
|
||||
|
||||
class SalesOrderReportSerializer(InvenTreeModelSerializer):
|
||||
class SalesOrderReportSerializer(ReportSerializerBase):
|
||||
"""Serializer class for the SalesOrderReport model"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = SalesOrderReport
|
||||
fields = [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'template',
|
||||
'filters',
|
||||
'enabled',
|
||||
]
|
||||
fields = ReportSerializerBase.report_fields()
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
class ReturnOrderReportSerializer(ReportSerializerBase):
|
||||
"""Serializer class for the ReturnOrderReport model"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
|
||||
model = ReturnOrderReport
|
||||
fields = ReportSerializerBase.report_fields()
|
||||
|
@ -0,0 +1,70 @@
|
||||
{% 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_version shortstring=True %}";
|
||||
{% 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 %}
|
@ -1,72 +1,10 @@
|
||||
{% extends "report/inventree_report_base.html" %}
|
||||
{% extends "report/inventree_order_report_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load report %}
|
||||
{% load barcode %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
margin-top: 4cm;
|
||||
{% endblock %}
|
||||
|
||||
{% block bottom_left %}
|
||||
content: "v{{report_revision}} - {{ date.isoformat }}";
|
||||
{% endblock %}
|
||||
|
||||
{% block bottom_center %}
|
||||
content: "{% inventree_version shortstring=True %}";
|
||||
{% 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 %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block header_content %}
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
{% extends "report/inventree_return_order_report_base.html" %}
|
@ -0,0 +1,62 @@
|
||||
{% extends "report/inventree_order_report_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load report %}
|
||||
{% load barcode %}
|
||||
{% load inventree_extras %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block header_content %}
|
||||
<img class='logo' src='{% company_image customer %}' alt="{{ customer }}" width='150'>
|
||||
|
||||
<div class='header-right'>
|
||||
<h3>{% trans "Return Order" %} {{ prefix }}{{ reference }}</h3>
|
||||
{% if customer %}{{ customer.name }}{% endif %}
|
||||
</div>
|
||||
{% endblock header_content %}
|
||||
|
||||
{% block page_content %}
|
||||
<h3>{% trans "Line Items" %}</h3>
|
||||
|
||||
<table class='table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Part" %}</th>
|
||||
<th>{% trans "Serial Number" %}</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.item.part %}' class='part-thumb'>
|
||||
</div>
|
||||
<div class='part-text'>
|
||||
{{ line.item.part.full_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ line.item.serial }}</td>
|
||||
<td>{{ line.reference }}</td>
|
||||
<td>{{ line.notes }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if extra_lines %}
|
||||
<tr><th colspan='4'>{% trans "Extra Line Items" %}</th></tr>
|
||||
{% for line in extra_lines.all %}
|
||||
<tr>
|
||||
<td><!-- No part --></td>
|
||||
<td><!-- No serial --></td>
|
||||
<td>{{ line.reference }}</td>
|
||||
<td>{{ line.notes }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
{% endblock page_content %}
|
@ -1,4 +1,4 @@
|
||||
{% extends "report/inventree_report_base.html" %}
|
||||
{% extends "report/inventree_order_report_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load report %}
|
||||
@ -6,69 +6,6 @@
|
||||
{% 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_version shortstring=True %}";
|
||||
{% 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'>
|
||||
|
@ -33,6 +33,15 @@ content: "{% trans 'Stock Item Test Report' %}";
|
||||
color: #F55;
|
||||
}
|
||||
|
||||
.test-not-found {
|
||||
color: #33A;
|
||||
}
|
||||
|
||||
.required-test-not-found {
|
||||
color: #EEE;
|
||||
background-color: #F55;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 5px;
|
||||
border: 1px solid;
|
||||
@ -84,7 +93,7 @@ content: "{% trans 'Stock Item Test Report' %}";
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if resul_list|length > 0 %}
|
||||
{% if test_keys|length > 0 %}
|
||||
<h3>{% trans "Test Results" %}</h3>
|
||||
|
||||
<table class='table test-table'>
|
||||
@ -101,22 +110,44 @@ content: "{% trans 'Stock Item Test Report' %}";
|
||||
<tr>
|
||||
<td colspan='5'><hr></td>
|
||||
</tr>
|
||||
{% for test in result_list %}
|
||||
{% for key in test_keys %}
|
||||
<!-- test key = {{ key }} -->
|
||||
{% getkey test_template_map key as test_template %}
|
||||
{% getkey results key as test_result %}
|
||||
<tr class='test-row'>
|
||||
<td>{{ test.test }}</td>
|
||||
{% if test.result %}
|
||||
<td>
|
||||
{% if test_template %}
|
||||
{% render_html_text test_template.test_name bold=test_template.required %}
|
||||
{% elif test_result %}
|
||||
{% render_html_text test_result.test italic=True %}
|
||||
{% else %}
|
||||
<!-- No matching test template or result for {{ key }} -->
|
||||
<span style='color: red;'>{{ key }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if test_result %}
|
||||
{% if test_result.result %}
|
||||
<td class='test-pass'>{% trans "Pass" %}</td>
|
||||
{% else %}
|
||||
<td class='test-fail'>{% trans "Fail" %}</td>
|
||||
{% endif %}
|
||||
<td>{{ test.value }}</td>
|
||||
<td>{{ test.user.username }}</td>
|
||||
<td>{{ test.date.date.isoformat }}</td>
|
||||
<td>{{ test_result.value }}</td>
|
||||
<td>{{ test_result.user.username }}</td>
|
||||
<td>{{ test_result.date.date.isoformat }}</td>
|
||||
{% else %}
|
||||
{% if test_template.required %}
|
||||
<td colspan='4' class='required-test-not-found'>{% trans "No result (required)" %}</td>
|
||||
{% else %}
|
||||
<td colspan='4' class='test-not-found'>{% trans "No result" %}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
{% else %}
|
||||
<em>No tests defined for this stock item</em>
|
||||
{% endif %}
|
||||
|
||||
{% if installed_items|length > 0 %}
|
||||
|
@ -19,17 +19,52 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def getkey(value: dict, arg):
|
||||
def getindex(container: list, index: int):
|
||||
"""Return the value contained at the specified index of the list.
|
||||
|
||||
This function is provideed to get around template rendering limitations.
|
||||
|
||||
Arguments:
|
||||
container: A python list object
|
||||
index: The index to retrieve from the list
|
||||
"""
|
||||
|
||||
# Index *must* be an integer
|
||||
try:
|
||||
index = int(index)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if index < 0 or index >= len(container):
|
||||
return None
|
||||
|
||||
try:
|
||||
value = container[index]
|
||||
except IndexError:
|
||||
value = None
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def getkey(container: dict, key):
|
||||
"""Perform key lookup in the provided dict object.
|
||||
|
||||
This function is provided to get around template rendering limitations.
|
||||
Ref: https://stackoverflow.com/questions/1906129/dict-keys-with-spaces-in-django-templates
|
||||
|
||||
Arguments:
|
||||
value: A python dict object
|
||||
arg: The 'key' to be found within the dict
|
||||
container: A python dict object
|
||||
key: The 'key' to be found within the dict
|
||||
"""
|
||||
return value[arg]
|
||||
if type(container) is not dict:
|
||||
logger.warning("getkey() called with non-dict object")
|
||||
return None
|
||||
|
||||
if key in container:
|
||||
return container[key]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
@ -215,3 +250,31 @@ def render_currency(money, **kwargs):
|
||||
"""Render a currency / Money object"""
|
||||
|
||||
return InvenTree.helpers.render_currency(money, **kwargs)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def render_html_text(text: str, **kwargs):
|
||||
"""Render a text item with some simple html tags.
|
||||
|
||||
kwargs:
|
||||
bold: Boolean, whether bold (or not)
|
||||
italic: Boolean, whether italic (or not)
|
||||
heading: str, heading level e.g. 'h3'
|
||||
"""
|
||||
|
||||
tags = []
|
||||
|
||||
if kwargs.get('bold', False):
|
||||
tags.append('strong')
|
||||
|
||||
if kwargs.get('italic', False):
|
||||
tags.append('em')
|
||||
|
||||
if heading := kwargs.get('heading', ''):
|
||||
tags.append(heading)
|
||||
|
||||
output = ''.join([f'<{tag}>' for tag in tags])
|
||||
output += text
|
||||
output += ''.join([f'</{tag}>' for tag in tags])
|
||||
|
||||
return mark_safe(output)
|
||||
|
@ -29,6 +29,20 @@ class ReportTagTest(TestCase):
|
||||
"""Enable or disable debug mode for reports"""
|
||||
InvenTreeSetting.set_setting('REPORT_DEBUG_MODE', value, change_user=None)
|
||||
|
||||
def test_getindex(self):
|
||||
"""Tests for the 'getindex' template tag"""
|
||||
|
||||
fn = report_tags.getindex
|
||||
data = [1, 2, 3, 4, 5, 6]
|
||||
|
||||
# Out of bounds or invalid
|
||||
self.assertEqual(fn(data, -1), None)
|
||||
self.assertEqual(fn(data, 99), None)
|
||||
self.assertEqual(fn(data, 'xx'), None)
|
||||
|
||||
for idx in range(len(data)):
|
||||
self.assertEqual(fn(data, idx), data[idx])
|
||||
|
||||
def test_getkey(self):
|
||||
"""Tests for the 'getkey' template tag"""
|
||||
|
||||
@ -419,7 +433,7 @@ class BOMReportTest(ReportTest):
|
||||
|
||||
|
||||
class PurchaseOrderReportTest(ReportTest):
|
||||
"""Unit test class fort he PurchaseOrderReport model"""
|
||||
"""Unit test class for the PurchaseOrderReport model"""
|
||||
model = report_models.PurchaseOrderReport
|
||||
|
||||
list_url = 'api-po-report-list'
|
||||
@ -446,3 +460,18 @@ class SalesOrderReportTest(ReportTest):
|
||||
self.copyReportTemplate('inventree_so_report.html', 'sales order report')
|
||||
|
||||
return super().setUp()
|
||||
|
||||
|
||||
class ReturnOrderReportTest(ReportTest):
|
||||
"""Unit tests for the ReturnOrderReport model"""
|
||||
|
||||
model = report_models.ReturnOrderReport
|
||||
list_url = 'api-return-order-report-list'
|
||||
detail_url = 'api-return-order-report-detail'
|
||||
print_url = 'api-return-order-report-print'
|
||||
|
||||
def setUp(self):
|
||||
"""Setup function for the ReturnOrderReport tests"""
|
||||
self.copyReportTemplate('inventree_return_order_report.html', 'return order report')
|
||||
|
||||
return super().setUp()
|
||||
|
Reference in New Issue
Block a user