├── tests ├── __init__.py ├── requirements.txt ├── templates │ └── base.html ├── urls.py ├── settings.py └── views.py ├── demo_proj ├── demo_app │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0005_product_size.py │ │ ├── 0003_product_category.py │ │ ├── 0004_client_country_product_sku.py │ │ ├── 0002_salestransaction_price_salestransaction_quantity.py │ │ ├── 0006_productcategory_remove_product_category_and_more.py │ │ └── 0001_initial.py │ ├── templatetags │ │ ├── __init__.py │ │ └── slick_reporting_demo_tags.py │ ├── tests.py │ ├── admin.py │ ├── apps.py │ ├── views.py │ ├── forms.py │ ├── models.py │ ├── helpers.py │ └── management │ │ └── commands │ │ └── create_entries.py ├── demo_proj │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── requirements.txt ├── templates │ ├── widget_template_with_pre.html │ ├── slick_reporting │ │ └── base.html │ ├── demo │ │ └── apex_report.html │ ├── dashboard.html │ ├── home.html │ └── base.html └── manage.py ├── slick_reporting ├── templatetags │ ├── __init__.py │ └── slick_reporting_tags.py ├── __init__.py ├── form_factory.py ├── apps.py ├── templates │ └── slick_reporting │ │ ├── js_resources.html │ │ ├── widget_template.html │ │ ├── base.html │ │ └── report.html ├── decorators.py ├── registry.py ├── helpers.py ├── static │ └── slick_reporting │ │ ├── slick_reporting.js │ │ ├── slick_reporting.datatable.js │ │ ├── slick_reporting.report_loader.js │ │ └── slick_reporting.chartsjs.js └── app_settings.py ├── setup.py ├── pyproject.toml ├── requirements.txt ├── docs ├── requirements.txt └── source │ ├── topics │ ├── _static │ │ ├── crosstab.png │ │ ├── timeseries.png │ │ ├── group_report.png │ │ └── list_view_form.png │ ├── structure.rst │ ├── list_report_options.rst │ ├── index.rst │ ├── exporting.rst │ ├── widgets.rst │ ├── filter_form.rst │ ├── integrating_slick_reporting.rst │ ├── crosstab_options.rst │ ├── charts.rst │ ├── group_by_report.rst │ ├── computation_field.rst │ └── time_series_options.rst │ ├── ref │ ├── index.rst │ ├── computation_field.rst │ ├── report_generator.rst │ └── settings.rst │ ├── conf.py │ ├── concept.rst │ ├── index.rst │ ├── howto │ ├── customize_frontend.rst │ └── index.rst │ └── tour.rst ├── MANIFEST.in ├── .readthedocs.yaml ├── .pre-commit-config.yaml ├── .github └── workflows │ └── django.yml ├── runtests.py ├── LICENSE.md ├── setup.cfg ├── .gitignore ├── README.rst └── CHANGELOG.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo_proj/demo_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo_proj/demo_proj/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo_proj/demo_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo_proj/demo_app/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /slick_reporting/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /demo_proj/demo_app/tests.py: -------------------------------------------------------------------------------- 1 | 2 | # Create your tests here. 3 | -------------------------------------------------------------------------------- /demo_proj/demo_app/admin.py: -------------------------------------------------------------------------------- 1 | 2 | # Register your models here. 3 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | -r ../requirements.txt 2 | crispy-bootstrap4 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | 4 | 5 | [tool.ruff] 6 | line-length = 120 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | python-dateutil>=2.8.1 3 | pytz 4 | simplejson 5 | django-crispy-forms -------------------------------------------------------------------------------- /demo_proj/requirements.txt: -------------------------------------------------------------------------------- 1 | django>=4.2 2 | python-dateutil>=2.8.1 3 | simplejson 4 | django-crispy-forms 5 | crispy-bootstrap5 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | -r ../requirements.txt 2 | crispy_bootstrap4 3 | sphinx 4 | sphinx_rtd_theme==1.3.0 5 | readthedocs-sphinx-search==0.3.1 -------------------------------------------------------------------------------- /docs/source/topics/_static/crosstab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RamezIssac/django-slick-reporting/HEAD/docs/source/topics/_static/crosstab.png -------------------------------------------------------------------------------- /docs/source/topics/_static/timeseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RamezIssac/django-slick-reporting/HEAD/docs/source/topics/_static/timeseries.png -------------------------------------------------------------------------------- /slick_reporting/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "slick_reporting.apps.ReportAppConfig" 2 | 3 | VERSION = (1, 3, 1) 4 | 5 | __version__ = "1.3.1" 6 | -------------------------------------------------------------------------------- /docs/source/topics/_static/group_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RamezIssac/django-slick-reporting/HEAD/docs/source/topics/_static/group_report.png -------------------------------------------------------------------------------- /docs/source/topics/_static/list_view_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RamezIssac/django-slick-reporting/HEAD/docs/source/topics/_static/list_view_form.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include README.md 3 | recursive-include slick_reporting/static * 4 | recursive-include slick_reporting/templates * 5 | recursive-exclude tests/ * -------------------------------------------------------------------------------- /demo_proj/demo_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DemoAppConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "demo_app" 7 | -------------------------------------------------------------------------------- /demo_proj/templates/widget_template_with_pre.html: -------------------------------------------------------------------------------- 1 | {% extends "slick_reporting/widget_template.html" %} 2 | {% block widget_content %} 3 |
4 |

5 |     
6 | {% endblock %} -------------------------------------------------------------------------------- /tests/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | {% block content %} 7 | {% endblock %} 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /slick_reporting/form_factory.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | # warn deprecated 4 | warnings.warn( 5 | "slick_reporting.form_factory is deprecated. Use slick_reporting.forms instead", 6 | Warning, 7 | stacklevel=2, 8 | ) 9 | 10 | from .forms import * # noqa 11 | -------------------------------------------------------------------------------- /slick_reporting/apps.py: -------------------------------------------------------------------------------- 1 | from django import apps 2 | 3 | 4 | class ReportAppConfig(apps.AppConfig): 5 | verbose_name = "Slick Reporting" 6 | name = "slick_reporting" 7 | 8 | def ready(self): 9 | super().ready() 10 | from . import fields # noqa 11 | -------------------------------------------------------------------------------- /demo_proj/demo_app/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | # Create your views here. 5 | 6 | class HomeView(TemplateView): 7 | template_name = "home.html" 8 | 9 | 10 | class Dashboard(TemplateView): 11 | template_name = "dashboard.html" 12 | -------------------------------------------------------------------------------- /docs/source/topics/structure.rst: -------------------------------------------------------------------------------- 1 | .. _structure: 2 | 3 | ================ 4 | Rows and columns 5 | ================ 6 | 7 | It's natural to think of a report as a form of tabular data, with rows and columns. 8 | 9 | We willexplore the ways one can create the rows and column of a report. 10 | 11 | a simple example 12 | -------------------------------------------------------------------------------- /demo_proj/templates/slick_reporting/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block meta_page_title %} {{ report_title }}{% endblock %} 4 | {% block page_title %} {{ report_title }} {% endblock %} 5 | 6 | {% block extrajs %} 7 | {{ block.super }} 8 | {% include "slick_reporting/js_resources.html" %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.11" 7 | 8 | # Build from the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | # Explicitly set the version of Python and its requirements 13 | python: 14 | install: 15 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /docs/source/ref/index.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | Reference 4 | =========== 5 | 6 | Below are links to the reference documentation for the various components of the Django slick reporting . 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :caption: Components: 11 | 12 | settings 13 | view_options 14 | computation_field 15 | report_generator 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /slick_reporting/templates/slick_reporting/js_resources.html: -------------------------------------------------------------------------------- 1 | {% load i18n static slick_reporting_tags %} 2 | 3 | {% get_slick_reporting_settings as slick_reporting_settings %} 4 | 5 | {% add_jquery %} 6 | {% get_slick_reporting_media as media %} 7 | {{ media }} 8 | 9 | 11 | 12 | {{ slick_reporting_settings|json_script:"slick_reporting_settings" }} 13 | -------------------------------------------------------------------------------- /demo_proj/demo_proj/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for demo_proj project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_proj.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | - repo: https://github.com/adamchainz/blacken-docs 4 | rev: "1.13.0" 5 | hooks: 6 | - id: blacken-docs 7 | additional_dependencies: 8 | - black==22.12.0 9 | 10 | - repo: https://github.com/psf/black 11 | rev: 23.3.0 12 | hooks: 13 | - id: black 14 | language_version: python3.9 15 | 16 | - repo: https://github.com/astral-sh/ruff-pre-commit 17 | # Ruff version. 18 | rev: v0.0.287 19 | hooks: 20 | - id: ruff 21 | -------------------------------------------------------------------------------- /demo_proj/demo_app/migrations/0005_product_size.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-30 11:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('demo_app', '0004_client_country_product_sku'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='size', 16 | field=models.CharField(default='Medium', max_length=100, verbose_name='Product Category'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /demo_proj/demo_app/migrations/0003_product_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-08-30 08:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("demo_app", "0002_salestransaction_price_salestransaction_quantity"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="product", 14 | name="category", 15 | field=models.CharField( 16 | default="Medium", max_length=100, verbose_name="Product Category" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /demo_proj/demo_proj/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo_proj project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os, sys 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_proj.settings_production") 15 | BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "../") 16 | sys.path.append(os.path.abspath(BASE_DIR)) 17 | 18 | application = get_wsgi_application() 19 | -------------------------------------------------------------------------------- /demo_proj/demo_app/migrations/0004_client_country_product_sku.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-30 08:38 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('demo_app', '0003_product_category'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='client', 16 | name='country', 17 | field=models.CharField(default='US', max_length=255, verbose_name='Country'), 18 | ), 19 | migrations.AddField( 20 | model_name='product', 21 | name='sku', 22 | field=models.CharField(default=uuid.uuid4, max_length=255, verbose_name='SKU'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | pull_request: 7 | branches: [ "develop" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.9, "3.10", 3.11] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v3 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r tests/requirements.txt 28 | - name: Run Tests 29 | run: | 30 | python runtests.py 31 | -------------------------------------------------------------------------------- /slick_reporting/decorators.py: -------------------------------------------------------------------------------- 1 | def report_field_register(report_field, *args, **kwargs): 2 | """ 3 | Registers the given model(s) classes and wrapped ModelAdmin class with 4 | admin site: 5 | 6 | @register(Author) 7 | class AuthorAdmin(admin.ModelAdmin): 8 | pass 9 | 10 | A kwarg of `site` can be passed as the admin site, otherwise the default 11 | admin site will be used. 12 | """ 13 | from .fields import ComputationField 14 | from .registry import field_registry 15 | 16 | def _model_admin_wrapper(admin_class): 17 | if not issubclass(admin_class, ComputationField): 18 | raise ValueError("Wrapped class must subclass ComputationField.") 19 | 20 | field_registry.register(report_field) 21 | 22 | _model_admin_wrapper(report_field) 23 | return report_field 24 | -------------------------------------------------------------------------------- /demo_proj/demo_app/migrations/0002_salestransaction_price_salestransaction_quantity.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-08-02 09:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("demo_app", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="salestransaction", 14 | name="price", 15 | field=models.DecimalField(decimal_places=2, default=0, max_digits=9), 16 | preserve_default=False, 17 | ), 18 | migrations.AddField( 19 | model_name="salestransaction", 20 | name="quantity", 21 | field=models.DecimalField(decimal_places=2, default=0, max_digits=9), 22 | preserve_default=False, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /docs/source/ref/computation_field.rst: -------------------------------------------------------------------------------- 1 | .. _computation_field_ref: 2 | 3 | ComputationField API 4 | -------------------- 5 | 6 | .. autoclass:: slick_reporting.fields.ComputationField 7 | 8 | .. autoattribute:: name 9 | .. autoattribute:: calculation_field 10 | .. autoattribute:: calculation_method 11 | .. autoattribute:: verbose_name 12 | .. autoattribute:: requires 13 | .. autoattribute:: type 14 | 15 | .. rubric:: Below are some data passed by the `ReportGenerator`, for extra manipulation, you can change them 16 | 17 | .. autoattribute:: report_model 18 | .. autoattribute:: group_by 19 | .. autoattribute:: plus_side_q 20 | .. autoattribute:: minus_side_q 21 | 22 | .. rubric:: You can customize those methods for maximum control where you can do pretty much whatever you want. 23 | 24 | .. automethod:: prepare 25 | .. automethod:: resolve 26 | .. automethod:: get_dependency_value 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /demo_proj/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_proj.settings") 11 | # add slick reporting to path so that it can be imported 12 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 13 | sys.path.append(os.path.abspath(BASE_DIR)) 14 | try: 15 | from django.core.management import execute_from_command_line 16 | except ImportError as exc: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) from exc 22 | execute_from_command_line(sys.argv) 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import argparse 6 | import django 7 | from django.conf import settings 8 | from django.test.utils import get_runner 9 | 10 | if __name__ == "__main__": 11 | parser = argparse.ArgumentParser( 12 | description="Run the Django Slick Reporting test suite." 13 | ) 14 | parser.add_argument( 15 | "modules", 16 | nargs="*", 17 | metavar="module", 18 | help='Optional path(s) to test modules; e.g. "i18n" or ' 19 | '"i18n.tests.TranslationTests.test_lazy_objects".', 20 | ) 21 | options = parser.parse_args() 22 | 23 | options.modules = [os.path.normpath(labels) for labels in options.modules] 24 | 25 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" 26 | django.setup() 27 | TestRunner = get_runner(settings) 28 | test_runner = TestRunner() 29 | failures = test_runner.run_tests(options.modules) 30 | # failures = test_runner.run_tests(["tests"]) 31 | sys.exit(bool(failures)) 32 | -------------------------------------------------------------------------------- /demo_proj/demo_proj/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for demo_proj project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path 19 | 20 | from demo_app import views 21 | from demo_app import helpers 22 | 23 | urlpatterns = helpers.get_urls_patterns() + [ 24 | path("", views.HomeView.as_view(), name="home"), 25 | path("dashboard/", views.Dashboard.as_view(), name="dashboard"), 26 | path("admin/", admin.site.urls), 27 | ] 28 | -------------------------------------------------------------------------------- /slick_reporting/templates/slick_reporting/widget_template.html: -------------------------------------------------------------------------------- 1 | {% load slick_reporting_tags %} 2 | 3 | 4 |
5 | {% if display_title %} 6 |
7 |
{{ title }}
8 |
9 | {% endif %} 10 |
11 |
19 | {% block widget_content %} 20 | {% if display_chart %} 21 |
22 | {% endif %} 23 | {% if display_table %} 24 |
25 |
26 | {% endif %} 27 | {% endblock %} 28 | 29 |
30 |
31 |
-------------------------------------------------------------------------------- /docs/source/topics/list_report_options.rst: -------------------------------------------------------------------------------- 1 | .. _list_reports: 2 | 3 | List Reports 4 | ============ 5 | 6 | 7 | It's a simple ListView / admin changelist like report to display data in a model. 8 | It's quite similar to ReportView except there is no calculation by default. 9 | 10 | Here is the options you can use to customize the report: 11 | 12 | #. ``columns``: a list of report_model fields to be displayed in the report, which support traversing 13 | 14 | .. code-block:: python 15 | 16 | class RequestLog(ListReportView): 17 | report_model = SalesTransaction 18 | columns = [ 19 | "id", 20 | "date", 21 | "client__name", 22 | "product__name", 23 | "quantity", 24 | "price", 25 | "value", 26 | ] 27 | 28 | 29 | #. ``filters``: a list of report_model fields to be used as filters. 30 | 31 | .. code-block:: python 32 | 33 | class RequestLog(ListReportView): 34 | report_model = SalesTransaction 35 | columns = [ 36 | "id", 37 | "date", 38 | "client__name", 39 | "product__name", 40 | "quantity", 41 | "price", 42 | "value", 43 | ] 44 | 45 | filters = ["product", "client"] 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path("report1/", views.MonthlyProductSales.as_view(), name="report1"), 6 | path( 7 | "product_crosstab_client/", 8 | views.ProductClientSalesMatrix.as_view(), 9 | name="product_crosstab_client", 10 | ), 11 | path( 12 | "report-to-field-set/", 13 | views.MonthlyProductSalesToFIeldSet.as_view(), 14 | name="report-to-field-set", 15 | ), 16 | path( 17 | "product_crosstab_client/", 18 | views.ProductClientSalesMatrix.as_view(), 19 | name="product_crosstab_client", 20 | ), 21 | path( 22 | "product_crosstab_client-to_field-set/", 23 | views.ProductClientSalesMatrixToFieldSet.as_view(), 24 | name="product_crosstab_client_to_field_set", 25 | ), 26 | path( 27 | "crosstab-columns-on-fly/", 28 | views.CrossTabColumnOnFly.as_view(), 29 | name="crosstab-columns-on-fly", 30 | ), 31 | path( 32 | "crosstab-columns-on-fly-to-field-set/", 33 | views.CrossTabColumnOnFlyToFieldSet.as_view(), 34 | name="crosstab-columns-on-fly-to-field-set", 35 | ), 36 | path( 37 | "queryset-only/", views.MonthlyProductSalesWQS.as_view(), name="queryset-only" 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /docs/source/topics/index.rst: -------------------------------------------------------------------------------- 1 | .. _topics: 2 | 3 | Topics 4 | ====== 5 | 6 | ReportView is a ``django.views.generic.FromView`` subclass that exposes the **Report Generator API** allowing you to create a report seamlessly in a view. 7 | 8 | * Exposes the report generation options in the view class. 9 | * Auto generate the filter form based on the report model, or uses your custom form to generate and filter the report. 10 | * Return an html page prepared to display the results in a table and charts. 11 | * Export to CSV, which is extendable to apply other exporting methods. (like yaml or other) 12 | * Print the report in a dedicated page design. 13 | 14 | 15 | You saw how to use the ReportView class in the tutorial and you identified the types of reports available, in the next section we will go in depth about: 16 | 17 | #. Each type of the reports and its options. 18 | #. The general options available for all report types 19 | #. How to customize the Form 20 | #. How to customize exports and print. 21 | 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | :caption: Topics: 26 | :titlesonly: 27 | 28 | 29 | group_by_report 30 | time_series_options 31 | crosstab_options 32 | list_report_options 33 | filter_form 34 | widgets 35 | integrating_slick_reporting 36 | charts 37 | exporting 38 | computation_field 39 | -------------------------------------------------------------------------------- /demo_proj/demo_app/migrations/0006_productcategory_remove_product_category_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-30 17:57 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('demo_app', '0005_product_size'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ProductCategory', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100, verbose_name='Product Category Name')), 19 | ], 20 | ), 21 | migrations.RemoveField( 22 | model_name='product', 23 | name='category', 24 | ), 25 | migrations.AlterField( 26 | model_name='product', 27 | name='size', 28 | field=models.CharField(default='Medium', max_length=100, verbose_name='Size'), 29 | ), 30 | migrations.AddField( 31 | model_name='product', 32 | name='product_category', 33 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='demo_app.productcategory'), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /slick_reporting/templates/slick_reporting/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | {% load crispy_forms_tags %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block extra_head %} 12 | 13 | 16 | 19 | {% endblock %} 20 | 21 | {{ report_title }} | Django Slick Reporting 22 | 23 | 24 |
25 | 26 | 27 |
28 |

{{ report_title }}

29 | {% block content %} 30 | 31 | {% endblock %} 32 |
33 |
34 | 35 | 36 | 37 | {% block extrajs %} 38 | {% include "slick_reporting/js_resources.html" %} 39 | 40 | {% endblock %} 41 | 42 | -------------------------------------------------------------------------------- /docs/source/ref/report_generator.rst: -------------------------------------------------------------------------------- 1 | .. _report_generator: 2 | 3 | Report Generator API 4 | ==================== 5 | 6 | The main class responsible generating the report and managing the flow 7 | 8 | 9 | ReportGenerator 10 | --------------- 11 | 12 | .. autoclass:: slick_reporting.generator.ReportGenerator 13 | 14 | .. rubric:: Below are the basic needed attrs 15 | .. autoattribute:: report_model 16 | .. autoattribute:: queryset 17 | .. autoattribute:: date_field 18 | .. autoattribute:: columns 19 | .. autoattribute:: group_by 20 | 21 | .. rubric:: Below are the needed attrs and methods for time series manipulation 22 | .. autoattribute:: time_series_pattern 23 | .. autoattribute:: time_series_columns 24 | .. automethod:: get_custom_time_series_dates 25 | .. automethod:: get_time_series_field_verbose_name 26 | 27 | .. rubric:: Below are the needed attrs and methods for crosstab manipulation 28 | .. autoattribute:: crosstab_field 29 | .. autoattribute:: crosstab_columns 30 | .. autoattribute:: crosstab_ids 31 | .. autoattribute:: crosstab_compute_remainder 32 | .. automethod:: get_crosstab_field_verbose_name 33 | 34 | .. rubric:: Below are the magical attrs 35 | .. autoattribute:: limit_records 36 | .. autoattribute:: swap_sign 37 | .. autoattribute:: field_registry_class 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /demo_proj/demo_app/templatetags/slick_reporting_demo_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.urls import reverse 3 | from django.utils.html import format_html 4 | from django.utils.safestring import mark_safe 5 | 6 | register = template.Library() 7 | 8 | 9 | def get_section(section): 10 | from ..helpers import TUTORIAL, GROUP_BY, TIME_SERIES, CROSSTAB 11 | to_use = [] 12 | 13 | if section == "tutorial": 14 | to_use = TUTORIAL 15 | elif section == "group_by": 16 | to_use = GROUP_BY 17 | elif section == "timeseries": 18 | to_use = TIME_SERIES 19 | elif section == "crosstab": 20 | to_use = CROSSTAB 21 | return to_use 22 | 23 | 24 | @register.simple_tag(takes_context=True) 25 | def get_menu(context, section): 26 | request = context['request'] 27 | to_use = get_section(section) 28 | menu = [] 29 | for link, report in to_use: 30 | is_active = "active" if f"/{link}/" in request.path else "" 31 | 32 | menu.append(format_html( 33 | '{text}', active=is_active, 34 | href=reverse(link), text=report.report_title or link) 35 | ) 36 | 37 | return mark_safe("".join(menu)) 38 | 39 | 40 | @register.simple_tag(takes_context=True) 41 | def should_show(context, section): 42 | request = context["request"] 43 | to_use = get_section(section) 44 | for link, report in to_use: 45 | if f"/{link}/" in request.path: 46 | return "show" 47 | return "" 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Ra Systems 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /slick_reporting/registry.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.contrib.admin.sites import AlreadyRegistered, NotRegistered 4 | 5 | 6 | class ReportFieldRegistry(object): 7 | def __init__(self): 8 | super(ReportFieldRegistry, self).__init__() 9 | self._registry = {} # holds 10 | 11 | def register(self, report_field, override=False): 12 | """ 13 | Register a report_field into the registry, 14 | :param report_field: 15 | :param override: if True, a report_field will get replaced if found, else it would throw an AlreadyRegistered 16 | :return: report_field passed 17 | """ 18 | if report_field.name in self._registry and not override: 19 | raise AlreadyRegistered(f"The field name {report_field.name} is used before and `override` is False") 20 | 21 | self._registry[report_field.name] = report_field 22 | return report_field 23 | 24 | def unregister(self, report_field): 25 | """ 26 | To unregister a Report Field 27 | :param report_field: a Report field class or a ReportField Name 28 | :return: None 29 | """ 30 | name = report_field if isinstance(report_field, str) else report_field.name 31 | if name not in self._registry: 32 | raise NotRegistered(report_field) 33 | del self._registry[name] 34 | 35 | def get_field_by_name(self, name): 36 | if name in self._registry: 37 | return self._registry[name] 38 | else: 39 | raise KeyError( 40 | f'{name} is not found in the report field registry. Options are {",".join(self.get_all_report_fields_names())}' 41 | ) 42 | 43 | def get_all_report_fields_names(self): 44 | return list(self._registry.keys()) 45 | 46 | 47 | field_registry = ReportFieldRegistry() 48 | -------------------------------------------------------------------------------- /demo_proj/demo_app/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.db.models import Q 3 | from slick_reporting.forms import BaseReportForm 4 | 5 | 6 | class TotalSalesFilterForm(BaseReportForm, forms.Form): 7 | PRODUCT_SIZE_CHOICES = ( 8 | ("all", "All"), 9 | ("big-only", "Big Only"), 10 | ("small-only", "Small Only"), 11 | ("medium-only", "Medium Only"), 12 | ("all-except-extra-big", "All except extra Big"), 13 | ) 14 | start_date = forms.DateField( 15 | required=False, 16 | label="Start Date", 17 | widget=forms.DateInput(attrs={"type": "date"}), 18 | ) 19 | end_date = forms.DateField( 20 | required=False, label="End Date", widget=forms.DateInput(attrs={"type": "date"}) 21 | ) 22 | product_size = forms.ChoiceField( 23 | choices=PRODUCT_SIZE_CHOICES, required=False, label="Product Size", initial="all" 24 | ) 25 | 26 | def get_filters(self): 27 | # return the filters to be used in the report 28 | # Note: the use of Q filters and kwargs filters 29 | kw_filters = {} 30 | q_filters = [] 31 | if self.cleaned_data["product_size"] == "big-only": 32 | kw_filters["product__size__in"] = ["extra_big", "big"] 33 | elif self.cleaned_data["product_size"] == "small-only": 34 | kw_filters["product__size__in"] = ["extra_small", "small"] 35 | elif self.cleaned_data["product_size"] == "medium-only": 36 | kw_filters["product__size__in"] = ["medium"] 37 | elif self.cleaned_data["product_size"] == "all-except-extra-big": 38 | q_filters.append(~Q(product__size__in=["extra_big", "big"])) 39 | return q_filters, kw_filters 40 | 41 | def get_start_date(self): 42 | return self.cleaned_data["start_date"] 43 | 44 | def get_end_date(self): 45 | return self.cleaned_data["end_date"] 46 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE.md 3 | name = django-slick-reporting 4 | version = attr: slick_reporting.__version__ 5 | author = Ra Systems 6 | author_email = ramez@rasystems.io 7 | description = A one-stop report and analytics generation and computation with batteries included 8 | long_description = file:README.rst 9 | long_description_content_type = text/x-rst 10 | url = https://django-slick-reporting.com/ 11 | project_urls = 12 | Travis CI = https://travis-ci.org/ra-systems/django-slick-reporting/ 13 | Documentation = https://django-slick-reporting.readthedocs.io/en/latest/ 14 | Source = https://github.com/ra-systems/django-slick-reporting 15 | classifiers = 16 | Environment :: Web Environment 17 | Framework :: Django 18 | Framework :: Django :: 2.2 19 | Framework :: Django :: 3.0 20 | Framework :: Django :: 3.1 21 | Framework :: Django :: 3.2 22 | Intended Audience :: Developers 23 | Development Status :: 5 - Production/Stable 24 | License :: OSI Approved :: BSD License 25 | Natural Language :: English 26 | Operating System :: MacOS :: MacOS X 27 | Operating System :: POSIX 28 | Operating System :: POSIX :: BSD 29 | Operating System :: POSIX :: Linux 30 | Operating System :: Microsoft :: Windows 31 | Programming Language :: Python 32 | Programming Language :: Python :: 3 33 | Programming Language :: Python :: 3 :: Only 34 | Programming Language :: Python :: 3.6 35 | Programming Language :: Python :: 3.7 36 | Programming Language :: Python :: 3.8 37 | Programming Language :: Python :: 3.9 38 | Topic :: Internet :: WWW/HTTP 39 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 40 | 41 | [options] 42 | include_package_data = true 43 | packages = find: 44 | 45 | python_requires = >=3.6 46 | install_requires = 47 | django>=2.2 48 | python-dateutil>2.8.1 49 | pytz 50 | simplejson 51 | django-crispy-forms 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /demo_proj/templates/demo/apex_report.html: -------------------------------------------------------------------------------- 1 | {% extends "slick_reporting/report.html" %} 2 | {% load slick_reporting_tags %} 3 | 4 | {% block content %} 5 | {{ block.super }} 6 | 7 | {% endblock %} 8 | 9 | {% block extrajs %} 10 | {{ block.super }} 11 | 12 | 63 | 64 | {% endblock %} -------------------------------------------------------------------------------- /docs/source/topics/exporting.rst: -------------------------------------------------------------------------------- 1 | Exporting 2 | ========= 3 | 4 | Exporting to CSV 5 | ----------------- 6 | To trigger an export to CSV, just add ``?_export=csv`` to the url. This is performed by by the Export to CSV button in the default form. 7 | 8 | This will call the export_csv on the view class, engaging a `ExportToStreamingCSV` 9 | 10 | Having an `_export` parameter not implemented, ie the view class do not implement ``export_{parameter_name}``, will be ignored. 11 | 12 | 13 | Configuring the CSV export option 14 | --------------------------------- 15 | 16 | You can disable the CSV export option by setting the ``csv_export_class`` attribute to ``False`` on the view class. 17 | and you can override the function and its attributes to customize the button text 18 | 19 | .. code-block:: python 20 | 21 | class CustomExportReport(GroupByReport): 22 | report_title = _("Custom Export Report") 23 | 24 | def export_csv(self, report_data): 25 | return super().export_csv(report_data) 26 | 27 | export_csv.title = _("My Custom CSV export Title") 28 | export_csv.css_class = "btn btn-success" 29 | 30 | 31 | Adding an export option 32 | ----------------------- 33 | 34 | You can extend the functionality, say you want to export to pdf. 35 | Add a ``export_pdf`` method to the view class, accepting the report_data json response and return the response you want. 36 | This ``export_pdf` will be called automatically when url parameter contain ``?_export=pdf`` 37 | 38 | 39 | Example to add a pdf export option: 40 | 41 | .. code-block:: python 42 | 43 | class CustomExportReport(GroupByReport): 44 | report_title = _("Custom Export Report") 45 | export_actions = ["export_pdf"] 46 | 47 | def export_pdf(self, report_data): 48 | return HttpResponse(f"Dummy PDF Exported {report_data}") 49 | 50 | export_pdf.title = _("Export PDF") 51 | export_pdf.icon = "fa fa-file-pdf-o" 52 | export_pdf.css_class = "btn btn-primary" 53 | 54 | The export function should accept the report_data json response and return the response you want. 55 | 56 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | SECRET_KEY = "fake-key" 4 | 5 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | BASE_DIR = os.path.dirname(PROJECT_DIR) 8 | 9 | DATABASES = { 10 | "default": { 11 | "ENGINE": "django.db.backends.sqlite3", 12 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 13 | "TEST": {"NAME": "tst_db.sqlite3", "MIGRATE": False}, 14 | }, 15 | } 16 | 17 | PASSWORD_HASHERS = [ 18 | "django.contrib.auth.hashers.MD5PasswordHasher", 19 | ] 20 | 21 | INSTALLED_APPS = [ 22 | # 'django.contrib.admin', 23 | "django.contrib.contenttypes", 24 | "django.contrib.auth", 25 | "django.contrib.sites", 26 | "django.contrib.sessions", 27 | "django.contrib.messages", 28 | # 'django.contrib.admin.apps.SimpleAdminConfig', 29 | "django.contrib.staticfiles", 30 | "slick_reporting", 31 | "crispy_forms", 32 | "crispy_bootstrap4", 33 | "tests", 34 | ] 35 | 36 | ROOT_URLCONF = "tests.urls" 37 | 38 | TEMPLATES = [ 39 | { 40 | "BACKEND": "django.template.backends.django.DjangoTemplates", 41 | "DIRS": [], 42 | "APP_DIRS": True, 43 | "OPTIONS": { 44 | "context_processors": [ 45 | "django.template.context_processors.debug", 46 | "django.template.context_processors.request", 47 | "django.contrib.auth.context_processors.auth", 48 | "django.contrib.messages.context_processors.messages", 49 | "django.template.context_processors.static", 50 | ], 51 | }, 52 | }, 53 | ] 54 | STATIC_URL = "/static/" 55 | 56 | MIGRATION_MODULES = {"contenttypes": None, "auth": None} 57 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 58 | 59 | CRISPY_TEMPLATE_PACK = "bootstrap4" 60 | 61 | 62 | MIDDLEWARE = [ 63 | "django.middleware.security.SecurityMiddleware", 64 | "django.contrib.sessions.middleware.SessionMiddleware", 65 | "django.middleware.common.CommonMiddleware", 66 | "django.middleware.csrf.CsrfViewMiddleware", 67 | "django.contrib.auth.middleware.AuthenticationMiddleware", 68 | ] 69 | -------------------------------------------------------------------------------- /slick_reporting/helpers.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django.conf import settings 4 | from django.contrib.contenttypes.fields import GenericForeignKey 5 | 6 | 7 | def get_calculation_annotation(calculation_field, calculation_method): 8 | """ 9 | Returns the default django annotation 10 | @param calculation_field: the field to calculate ex 'value' 11 | @param calculation_method: the aggregation method ex: Sum 12 | @return: the annotation ex value__sum 13 | """ 14 | 15 | return "__".join([calculation_field.lower(), calculation_method.name.lower()]) 16 | 17 | 18 | def get_foreign_keys(model): 19 | """ 20 | Scans a model and return an Ordered Dictionary with the foreign keys found 21 | :param model: the model to scan 22 | :return: Ordered Dict 23 | """ 24 | from django.db import models 25 | 26 | fields = model._meta.get_fields() 27 | fkeys = OrderedDict() 28 | for f in fields: 29 | if ( 30 | f.is_relation 31 | and type(f) is not models.OneToOneRel 32 | and type(f) is not models.ManyToOneRel 33 | and type(f) is not models.ManyToManyRel 34 | and type(f) is not GenericForeignKey 35 | ): 36 | fkeys[f.attname] = f 37 | return fkeys 38 | 39 | 40 | def get_field_from_query_text(path, model): 41 | """ 42 | return the field of a query text 43 | `modelA__modelB__foo_field` would return foo_field on modelsB 44 | :param path: 45 | :param model: 46 | :return: 47 | """ 48 | relations = path.split("__") 49 | _rel = model 50 | field = None 51 | for i, m in enumerate(relations): 52 | field = _rel._meta.get_field(m) 53 | if i == len(relations) - 1: 54 | return field 55 | _rel = field.related_model 56 | return field 57 | 58 | 59 | def user_test_function(report_view): 60 | """ 61 | A default test function return True on DEBUG, otherwise return the user.is_superuser 62 | :param report_view: 63 | :return: 64 | """ 65 | if not settings.DEBUG: 66 | return report_view.request.user.is_superuser 67 | return True 68 | -------------------------------------------------------------------------------- /slick_reporting/templates/slick_reporting/report.html: -------------------------------------------------------------------------------- 1 | {% extends 'slick_reporting/base.html' %} 2 | {% load crispy_forms_tags i18n slick_reporting_tags %} 3 | 4 | {% block content %} 5 |
6 | {% if form %} 7 |
8 |
9 |

{% trans "Filters" %}

10 |
11 |
12 | {% crispy form crispy_helper %} 13 |
14 | 26 |
27 | {% endif %} 28 | 29 |
30 |
31 |
{% trans "Results" %}
32 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 | {% endblock %} 50 | 51 | {% block extrajs %} 52 | {{ block.super }} 53 | {% get_charts_media report.get_chart_settings %} 54 | {% endblock %} -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | import django 16 | 17 | sys.path.insert(0, os.path.abspath("../../")) 18 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" 19 | django.setup() 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "Django Slick Reporting" 24 | copyright = "2020, Ramez Ashraf" 25 | author = "Ramez Ashraf" 26 | 27 | master_doc = "index" 28 | 29 | # The full version, including alpha/beta/rc tags 30 | release = "0.6.8" 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | autosummary_generate = True 38 | autoclass_content = "class" 39 | extensions = [ 40 | "sphinx.ext.viewcode", 41 | "sphinx.ext.autodoc", 42 | "sphinx.ext.autosummary", 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ["_templates"] 47 | 48 | # List of patterns, relative to source directory, that match files and 49 | # directories to ignore when looking for source files. 50 | # This pattern also affects html_static_path and html_extra_path. 51 | exclude_patterns = [] 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = "sphinx_rtd_theme" 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = ["_static"] 64 | -------------------------------------------------------------------------------- /demo_proj/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load slick_reporting_tags %} 3 | {% block page_title %} Dashboard {% endblock %} 4 | {% block meta_page_title %} Dashboard {% endblock %} 5 | 6 | 7 | {% block content %} 8 |
9 |
10 | {% get_widget_from_url url_name="product-sales" %} 11 |
12 |
13 | {% get_widget_from_url url_name="total-product-sales-by-country" title="Widget custom title" %} 14 |
15 | 16 |
17 | {% get_widget_from_url url_name="total-product-sales" chart_id=1 title="Custom default Chart" %} 18 |
19 | 20 |
21 | {% get_widget_from_url url_name="monthly-product-sales" chart_id=1 display_table=False title="No table, Chart Only" %} 22 |
23 | 24 |
25 | {% get_widget_from_url url_name="total-product-sales" display_chart=False title="Table only, no chart" %} 26 |
27 | 28 |
29 | {% get_widget_from_url url_name="total-product-sales" display_table=False display_chart_selector=False title="No Chart Selector, only the assigned one" %} 30 |
31 | 32 |
33 | {% get_widget_from_url url_name="total-product-sales" success_callback="custom_js_callback" title="Custom Js Handler and template" template_name="widget_template_with_pre.html" %} 34 |
35 | 36 | 37 |
38 | 39 | {% endblock %} 40 | 41 | {% block extrajs %} 42 | {% include "slick_reporting/js_resources.html" %} 43 | {# make sure to have the js_resources added to the dashboard page #} 44 | 45 | {% get_charts_media "all" %} 46 | {# make sure to add all charts needed media, here the "all" arguments add all charts media to the page, #} 47 | {# You can skip it and add needed media by hand #} 48 | 49 | 50 | 61 | {% endblock %} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | fabfile.py 131 | -------------------------------------------------------------------------------- /demo_proj/demo_app/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | # Create your models here. 8 | class Client(models.Model): 9 | name = models.CharField(max_length=100, verbose_name="Client Name") 10 | country = models.CharField(_("Country"), max_length=255, default="US") 11 | 12 | class Meta: 13 | verbose_name = _("Client") 14 | verbose_name_plural = _("Clients") 15 | 16 | def __str__(self): 17 | return self.name 18 | 19 | 20 | class ProductCategory(models.Model): 21 | name = models.CharField(max_length=100, verbose_name="Product Category Name") 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | 27 | class Product(models.Model): 28 | name = models.CharField(max_length=100, verbose_name="Product Name") 29 | # category = models.CharField(max_length=100, verbose_name="Product Category", default="Medium") 30 | product_category = models.ForeignKey(ProductCategory, on_delete=models.CASCADE, null=True) 31 | 32 | sku = models.CharField(_("SKU"), max_length=255, default=uuid.uuid4) 33 | size = models.CharField(max_length=100, verbose_name="Size", default="Medium") 34 | 35 | class Meta: 36 | verbose_name = _("Product") 37 | verbose_name_plural = _("Products") 38 | 39 | def __str__(self): 40 | return self.name 41 | 42 | 43 | class SalesTransaction(models.Model): 44 | number = models.CharField(max_length=100, verbose_name="Sales Transaction #") 45 | date = models.DateTimeField() 46 | notes = models.TextField(blank=True, null=True) 47 | client = models.ForeignKey( 48 | Client, on_delete=models.PROTECT, verbose_name=_("Client") 49 | ) 50 | product = models.ForeignKey( 51 | Product, on_delete=models.PROTECT, verbose_name=_("Product") 52 | ) 53 | value = models.DecimalField(max_digits=9, decimal_places=2) 54 | quantity = models.DecimalField(max_digits=9, decimal_places=2) 55 | price = models.DecimalField(max_digits=9, decimal_places=2) 56 | 57 | class Meta: 58 | verbose_name = _("Sales Transaction") 59 | verbose_name_plural = _("Sales Transactions") 60 | 61 | def __str__(self): 62 | return f"{self.number} - {self.date}" 63 | 64 | def save( 65 | self, force_insert=False, force_update=False, using=None, update_fields=None 66 | ): 67 | self.value = self.price * self.quantity 68 | super().save(force_insert, force_update, using, update_fields) 69 | -------------------------------------------------------------------------------- /demo_proj/demo_app/helpers.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import reports 4 | 5 | TUTORIAL = [ 6 | ("product-sales", reports.ProductSales), 7 | ("total-product-sales", reports.TotalProductSales), 8 | ("total-product-sales-by-country", reports.TotalProductSalesByCountry), 9 | ("monthly-product-sales", reports.MonthlyProductSales), 10 | ("product-sales-per-client-crosstab", reports.ProductSalesPerClientCrosstab), 11 | ("product-sales-per-country-crosstab", reports.ProductSalesPerCountryCrosstab), 12 | ("last-10-sales", reports.LastTenSales), 13 | ("total-product-sales-with-custom-form", reports.TotalProductSalesWithCustomForm), 14 | ] 15 | 16 | GROUP_BY = [ 17 | ("group-by-report", reports.GroupByReport), 18 | ("group-by-traversing-field", reports.GroupByTraversingFieldReport), 19 | ("group-by-custom-queryset", reports.GroupByCustomQueryset), 20 | ("no-group-by", reports.NoGroupByReport), 21 | ] 22 | 23 | TIME_SERIES = [ 24 | ("time-series-report", reports.TimeSeriesReport), 25 | ("time-series-with-selector", reports.TimeSeriesReportWithSelector), 26 | ("time-series-with-custom-dates", reports.TimeSeriesReportWithCustomDates), 27 | ("time-series-with-custom-dates-and-title", reports.TimeSeriesReportWithCustomDatesAndCustomTitle), 28 | ("time-series-without-group-by", reports.TimeSeriesWithoutGroupBy), 29 | ("time-series-with-group-by-custom-queryset", reports.TimeSeriesReportWithCustomGroupByQueryset), 30 | ] 31 | 32 | CROSSTAB = [ 33 | ("crosstab-report", reports.CrosstabReport), 34 | ("crosstab-report-with-ids", reports.CrosstabWithIds), 35 | ("crosstab-report-traversing-field", reports.CrosstabWithTraversingField), 36 | ("crosstab-report-custom-filter", reports.CrosstabWithIdsCustomFilter), 37 | ("crosstab-report-custom-verbose-name", reports.CrossTabReportWithCustomVerboseName), 38 | ("crosstab-report-custom-verbose-name-2", reports.CrossTabReportWithCustomVerboseNameCustomFilter), 39 | ("crosstab-report-with-time-series", reports.CrossTabWithTimeSeries), 40 | ] 41 | OTHER = [ 42 | ("highcharts-examples", reports.HighChartExample), 43 | ("chartjs-examples", reports.ChartJSExample), 44 | ("apexcharts-examples", reports.ProductSalesApexChart), 45 | ("custom-export", reports.CustomExportReport), 46 | ("form-initial", reports.ReportWithFormInitial), 47 | ] 48 | 49 | 50 | def get_urls_patterns(): 51 | urls = [] 52 | for name, report in TUTORIAL + GROUP_BY + TIME_SERIES + CROSSTAB + OTHER: 53 | urls.append(path(f"{name}/", report.as_view(), name=name)) 54 | return urls 55 | -------------------------------------------------------------------------------- /docs/source/concept.rst: -------------------------------------------------------------------------------- 1 | .. _structure: 2 | 3 | Welcome to Django Slick Reporting documentation! 4 | ================================================== 5 | 6 | Django Slick Reporting a reporting engine allowing you to create and chart different kind of analytics from your model in a breeze. 7 | 8 | Demo site 9 | --------- 10 | 11 | If you haven't yet, please check https://django-slick-reporting.com for a quick walk-though with live code examples.. 12 | 13 | 14 | 15 | :ref:`Tutorial ` 16 | -------------------------- 17 | 18 | The tutorial will guide you to what is slick reporting, what kind of reports it can do for you and how to use it in your project. 19 | 20 | 21 | 22 | :ref:`Topic Guides ` 23 | ---------------------------- 24 | 25 | Discuss each type of report main structures you can create with Django Slick Reporting and their options. 26 | 27 | * :ref:`Group By report `: Similar to what we'd do with a GROUP BY sql statement. We group by a field and do some kind of calculations over the grouped records. 28 | * :ref:`time_series`: A step further, where the calculations are computed for time periods (day, week, month, custom etc). 29 | * :ref:`crosstab_reports`: Where the results shows the relationship between two or more variables. It's a table that shows the distribution of one variable in rows and another in columns. 30 | * :ref:`list_reports`: Similar to a django admin's changelist, it's a direct view of the report model records 31 | * And other topics like how to customize the form, and extend the exporting options. 32 | 33 | 34 | :ref:`Reference ` 35 | ---------------------------- 36 | 37 | Detailed information about main on Django Slick Reporting's main components 38 | 39 | #. :ref:`Settings `: The settings you can use to customize the behavior of Django Slick Reporting. 40 | #. :ref:`Report View `: A ``FormView`` CBV subclass with reporting capabilities allowing you to create different types of reports in the view. 41 | It provide a default :ref:`Filter Form ` to filter the report on. 42 | It mimics the Generator API interface, so knowing one is enough to work with the other. 43 | 44 | #. :ref:`Generator `: Responsible for generating report and orchestrating and calculating the computation fields values and mapping them to the results. 45 | It has an intuitive API that allows you to define the report structure and the computation fields to be calculated. 46 | 47 | #. :ref:`Computation Field `: a calculation unit,like a Sum or a Count of a certain field. 48 | Computation field class set how the calculation should be done. ComputationFields can also depend on each other. 49 | 50 | #. Charting JS helpers: Highcharts and Charts js helpers libraries to plot the data generated. so you can create the chart in 1 line in the view 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Django Slick Reporting 2 | ====================== 3 | 4 | **Django Slick Reporting** a reporting engine allowing you to create & display diverse analytics. Batteries like a ready to use View and Highcharts & Charts.js integration are included. 5 | 6 | * Create group by , crosstab , timeseries, crosstab in timeseries and list reports in handful line with intuitive syntax 7 | * Highcharts & Charts.js integration ready to use with the shipped in View, easily extendable to use with your own charts. 8 | * Export to CSV 9 | * Easily extendable to add your own computation fields, 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | To install django-slick-reporting with pip 16 | 17 | .. code-block:: bash 18 | 19 | pip install django-slick-reporting 20 | 21 | 22 | Usage 23 | ----- 24 | 25 | #. Add ``"slick_reporting", "crispy_forms", "crispy_bootstrap4",`` to ``INSTALLED_APPS``. 26 | #. Add ``CRISPY_TEMPLATE_PACK = "bootstrap4"`` to your ``settings.py`` 27 | #. Execute `python manage.py collectstatic` so the JS helpers are collected and served. 28 | 29 | 30 | 31 | Quickstart 32 | ---------- 33 | 34 | You can start by using ``ReportView`` which is a subclass of ``django.views.generic.FormView`` 35 | 36 | .. code-block:: python 37 | 38 | # in views.py 39 | from slick_reporting.views import ReportView, Chart 40 | from slick_reporting.fields import ComputationField 41 | from .models import MySalesItems 42 | from django.db.models import Sum 43 | 44 | 45 | class ProductSales(ReportView): 46 | 47 | report_model = MySalesItems 48 | date_field = "date_placed" 49 | group_by = "product" 50 | 51 | columns = [ 52 | "title", 53 | ComputationField.create( 54 | method=Sum, field="value", name="value__sum", verbose_name="Total sold $" 55 | ), 56 | ] 57 | 58 | # Charts 59 | chart_settings = [ 60 | Chart( 61 | "Total sold $", 62 | Chart.BAR, 63 | data_source=["value__sum"], 64 | title_source=["title"], 65 | ), 66 | ] 67 | 68 | 69 | # in urls.py 70 | from django.urls import path 71 | from .views import ProductSales 72 | 73 | urlpatterns = [ 74 | path("product-sales/", ProductSales.as_view(), name="product-sales"), 75 | ] 76 | 77 | Demo site 78 | ---------- 79 | 80 | https://django-slick-reporting.com is a quick walk-though with live code examples 81 | 82 | 83 | 84 | Next step :ref:`tutorial` 85 | 86 | .. toctree:: 87 | :maxdepth: 2 88 | :caption: Contents: 89 | 90 | concept 91 | tutorial 92 | topics/index 93 | ref/index 94 | 95 | 96 | 97 | Indices and tables 98 | ================== 99 | 100 | * :ref:`genindex` 101 | * :ref:`modindex` 102 | * :ref:`search` 103 | 104 | -------------------------------------------------------------------------------- /slick_reporting/templatetags/slick_reporting_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.loader import get_template 3 | from django.forms import Media 4 | from django.urls import reverse, resolve 5 | from django.utils.safestring import mark_safe 6 | 7 | from ..app_settings import SLICK_REPORTING_JQUERY_URL, SLICK_REPORTING_SETTINGS, get_media 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.simple_tag 13 | def get_widget_from_url(url_name=None, url=None, **kwargs): 14 | _url = "" 15 | if not (url_name or url): 16 | raise ValueError("url_name or url must be provided") 17 | if url_name: 18 | url = reverse(url_name) 19 | view = resolve(url) 20 | kwargs["report"] = view.func.view_class 21 | kwargs["report_url"] = url 22 | return get_widget(**kwargs) 23 | 24 | 25 | @register.simple_tag 26 | def get_widget(report, template_name="", url_name="", report_url=None, **kwargs): 27 | kwargs["report"] = report 28 | if not report: 29 | raise ValueError("report argument is empty. Are you sure you're using the correct report name") 30 | if not (report_url or url_name): 31 | raise ValueError("report_url or url_name must be provided") 32 | 33 | # if not report.chart_settings: 34 | kwargs.setdefault("display_chart", bool(report.chart_settings)) 35 | kwargs.setdefault("display_table", True) 36 | 37 | kwargs.setdefault("display_chart_selector", kwargs["display_chart"]) 38 | kwargs.setdefault("display_title", True) 39 | 40 | passed_title = kwargs.get("title", None) 41 | kwargs["title"] = passed_title or report.get_report_title() 42 | kwargs["report_url"] = report_url 43 | if not report_url: 44 | kwargs["report_url"] = reverse(url_name) 45 | 46 | kwargs.setdefault("extra_params", "") 47 | 48 | template = get_template(template_name or "slick_reporting/widget_template.html") 49 | 50 | return template.render(context=kwargs) 51 | 52 | 53 | @register.simple_tag 54 | def add_jquery(): 55 | if SLICK_REPORTING_JQUERY_URL: 56 | return mark_safe(f'') 57 | return "" 58 | 59 | 60 | @register.simple_tag 61 | def get_charts_media(chart_settings): 62 | charts_dict = SLICK_REPORTING_SETTINGS["CHARTS"] 63 | media = Media() 64 | if chart_settings == "all": 65 | available_types = charts_dict.keys() 66 | else: 67 | available_types = [chart["engine_name"] for chart in chart_settings] 68 | available_types = set(available_types) 69 | 70 | for type in available_types: 71 | media += Media(css=charts_dict.get(type, {}).get("css", {}), js=charts_dict.get(type, {}).get("js", [])) 72 | return media 73 | 74 | 75 | @register.simple_tag 76 | def get_slick_reporting_media(): 77 | from django.forms import Media 78 | 79 | media = get_media() 80 | return Media(css=media["css"], js=media["js"]) 81 | 82 | 83 | @register.simple_tag 84 | def get_slick_reporting_settings(): 85 | return dict(SLICK_REPORTING_SETTINGS) 86 | -------------------------------------------------------------------------------- /demo_proj/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

Welcome to Django Slick Reporting

7 |

The Reporting Engine for Django.

8 | 9 |

10 | {# Start walk through#} 11 | Github 12 |

13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 |

Powerful

21 |

Effortlessly create Simple, Grouped, Time series and Crosstab reports in a handful of code lines. 22 | You can also create your Custom Calculation easily, which will be integrated with the above reports 23 | types

24 |

25 | {# This#} 26 | {# site on Github »#} 27 | Begin Walk through 28 |

29 |
30 |
31 |

Chart Wrappers

32 |

Slick reporting comes with Highcharts and Charts.js wrappers to transform the generated data into 34 | attractive charts in handfule of lines

35 |

You can check Django Slick Reporting documentation for more in depth information

36 |

Read 37 | the docs »

38 |
39 |
40 |

Open source

41 |

Optimized for speed. You can also check this same website and generate more data and test this package on million on records yourself 42 | 43 |

This 44 | site on Github »

45 | 46 | {# Star#} 49 |

50 | {#

View details »

#} 51 |
52 |
53 | 54 |
55 | 56 |
57 | 58 | 59 | {% endblock %} -------------------------------------------------------------------------------- /demo_proj/demo_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-08-02 09:14 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Client", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("name", models.CharField(max_length=100, verbose_name="Client Name")), 26 | ], 27 | options={ 28 | "verbose_name": "Client", 29 | "verbose_name_plural": "Clients", 30 | }, 31 | ), 32 | migrations.CreateModel( 33 | name="Product", 34 | fields=[ 35 | ( 36 | "id", 37 | models.BigAutoField( 38 | auto_created=True, 39 | primary_key=True, 40 | serialize=False, 41 | verbose_name="ID", 42 | ), 43 | ), 44 | ("name", models.CharField(max_length=100, verbose_name="Product Name")), 45 | ], 46 | options={ 47 | "verbose_name": "Product", 48 | "verbose_name_plural": "Products", 49 | }, 50 | ), 51 | migrations.CreateModel( 52 | name="SalesTransaction", 53 | fields=[ 54 | ( 55 | "id", 56 | models.BigAutoField( 57 | auto_created=True, 58 | primary_key=True, 59 | serialize=False, 60 | verbose_name="ID", 61 | ), 62 | ), 63 | ( 64 | "number", 65 | models.CharField( 66 | max_length=100, verbose_name="Sales Transaction #" 67 | ), 68 | ), 69 | ("date", models.DateTimeField()), 70 | ("notes", models.TextField(blank=True, null=True)), 71 | ("value", models.DecimalField(decimal_places=2, max_digits=9)), 72 | ( 73 | "client", 74 | models.ForeignKey( 75 | on_delete=django.db.models.deletion.PROTECT, 76 | to="demo_app.client", 77 | verbose_name="Client", 78 | ), 79 | ), 80 | ( 81 | "product", 82 | models.ForeignKey( 83 | on_delete=django.db.models.deletion.PROTECT, 84 | to="demo_app.product", 85 | verbose_name="Product", 86 | ), 87 | ), 88 | ], 89 | options={ 90 | "verbose_name": "Sales Transaction", 91 | "verbose_name_plural": "Sales Transactions", 92 | }, 93 | ), 94 | ] 95 | -------------------------------------------------------------------------------- /demo_proj/demo_app/management/commands/create_entries.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | from datetime import timedelta 4 | 5 | from django.contrib.auth import get_user_model 6 | from django.core.management.base import BaseCommand 7 | 8 | # from expense.models import Expense, ExpenseTransaction 9 | from ...models import Client, Product, SalesTransaction, ProductCategory 10 | 11 | User = get_user_model() 12 | 13 | 14 | def date_range(start_date, end_date): 15 | for i in range((end_date - start_date).days + 1): 16 | yield start_date + timedelta(i) 17 | 18 | 19 | class Command(BaseCommand): 20 | help = "Create Sample entries for the demo app" 21 | 22 | def handle(self, *args, **options): 23 | # create clients 24 | client_countries = [ 25 | "US", 26 | "DE", 27 | "EG", 28 | "IN", 29 | "KW", 30 | "RA" 31 | ] 32 | product_category = [ 33 | "extra_big", 34 | "big", 35 | "medium", 36 | "small", 37 | "extra-small" 38 | ] 39 | SalesTransaction.objects.all().delete() 40 | Client.objects.all().delete() 41 | Product.objects.all().delete() 42 | ProductCategory.objects.all().delete() 43 | User.objects.filter(is_superuser=False).delete() 44 | for i in range(10): 45 | User.objects.create_user(username=f"user {i}", password="password") 46 | 47 | list(User.objects.values_list("id", flat=True)) 48 | for i in range(1, 4): 49 | ProductCategory.objects.create(name=f"Product Category {i}") 50 | 51 | product_category_ids = list(ProductCategory.objects.values_list("id", flat=True)) 52 | for i in range(1, 10): 53 | Client.objects.create(name=f"Client {i}", 54 | country=random.choice(client_countries), 55 | # owner_id=random.choice(users_id) 56 | ) 57 | clients_ids = list(Client.objects.values_list("pk", flat=True)) 58 | # create products 59 | for i in range(1, 10): 60 | Product.objects.create(name=f"Product {i}", 61 | product_category_id=random.choice(product_category_ids), 62 | size=random.choice(product_category)) 63 | products_ids = list(Product.objects.values_list("pk", flat=True)) 64 | 65 | current_year = datetime.datetime.today().year 66 | start_date = datetime.datetime(current_year, 1, 1) 67 | end_date = datetime.datetime(current_year + 1, 1, 1) 68 | 69 | for date in date_range(start_date, end_date): 70 | for i in range(1, 10): 71 | SalesTransaction.objects.create( 72 | client_id=random.choice(clients_ids), 73 | product_id=random.choice(products_ids), 74 | quantity=random.randint(1, 10), 75 | price=random.randint(1, 100), 76 | date=date, 77 | number=f"Sale {date.strftime('%Y-%m-%d')} #{i}", 78 | ) 79 | # ExpenseTransaction.objects.create( 80 | # expense_id=random.choice(expense_ids), 81 | # value=random.randint(1, 100), 82 | # date=date, 83 | # number=f"Expense {date.strftime('%Y-%m-%d')} #{i}", 84 | # ) 85 | 86 | self.stdout.write(self.style.SUCCESS("Entries Created Successfully")) 87 | -------------------------------------------------------------------------------- /demo_proj/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block meta_page_title %}{{ report_title }}{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 17 | 18 | 19 | 43 | 44 | 45 | {# #} 46 |
47 | 48 | 64 |
65 | 78 |
79 |
80 | {% block content %} 81 | {% endblock %} 82 |
83 | 84 |
85 |
86 |
87 | 88 | 89 | 90 | {# #} 91 | {# #} 92 | 93 | 94 | {% block extrajs %} 95 | {% endblock %} 96 | 97 | -------------------------------------------------------------------------------- /docs/source/topics/widgets.rst: -------------------------------------------------------------------------------- 1 | .. _widgets: 2 | .. _dashboard: 3 | 4 | Dashboards 5 | ========== 6 | You can use the report data and charts on any other page, for example to create a dashboard. 7 | A dashboard page is a collection of report results / charts / tables. 8 | 9 | Adding a widget to a page is as easy as this code 10 | 11 | .. code-block:: html+django 12 | 13 | {% load static slick_reporting_tags %} 14 | 15 | {% block content %} 16 |
17 | {% get_widget_from_url url_name="product-sales" %} 18 |
19 | {% endblock %} 20 | 21 | {% block extrajs %} 22 | {% include "slick_reporting/js_resources.html" %} 23 | {% get_charts_media "all" %} 24 | {% endblock %} 25 | 26 | The `get_widget_from_url` with create a card block, which will contain the report results and charts. You can customize the widget by passing arguments to the template tag. 27 | 28 | Arguments 29 | --------- 30 | 31 | * title: string, a title for the widget, default to the report title. 32 | * chart_id: the id of the chart that will be rendered as default. 33 | chart_id is, by default, its index in the ``chart_settings`` list. 34 | * display_table: bool, If the widget should show the results table. 35 | * display_chart: bool, If the widget should show the chart. 36 | * display_chart_selector: bool, If the widget should show the chart selector links or just display the default,or the set chart_id, chart. 37 | * success_callback: string, the name of a javascript function that will be called after the report data is retrieved. 38 | * failure_callback: string, the name of a javascript function that will be called if the report data retrieval fails. 39 | * template_name: string, the template name used to render the widget. Default to `slick_reporting/widget_template.html` 40 | * extra_params: string, extra parameters to pass to the report. 41 | * report_form_selector: string, a jquery selector that will be used to find the form that will be used to pass extra parameters to the report. 42 | 43 | 44 | This code above will be actually rendered as this in the html page: 45 | 46 | .. code-block:: html+django 47 | 48 |
49 |
50 |
> 51 | 52 | 53 |
54 |
55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 | 63 | The ``data-report-widget`` attribute is used by the javascript to find the widget and render the report. 64 | The ``data-report-chart`` attribute is used by the javascript to find the chart container and render the chart and the chart selector. 65 | The ``data-report-table`` attribute is used by the javascript to find the table container and render the table. 66 | 67 | 68 | Customization Example 69 | --------------------- 70 | 71 | You You can customize how the widget is loading by defining your own success call-back 72 | and fail call-back functions. 73 | 74 | The success call-back function will receive the report data as a parameter 75 | 76 | 77 | .. code-block:: html+django 78 | 79 | {% load i18n static slick_reporting_tags %} 80 | 81 | {% get_widget_from_url url_name="product-sales" success_callback=my_success_callback %} 82 | 83 | 89 | 90 | 91 | Live example: 92 | ------------- 93 | 94 | You can see a live example of the widgets in the `Demo project- Dashboard Page `_. 95 | -------------------------------------------------------------------------------- /slick_reporting/static/slick_reporting/slick_reporting.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 3 | function executeFunctionByName(functionName, context /*, args */) { 4 | let args = Array.prototype.slice.call(arguments, 2); 5 | let namespaces = functionName.split("."); 6 | let func = namespaces.pop(); 7 | for (let i = 0; i < namespaces.length; i++) { 8 | context = context[namespaces[i]]; 9 | } 10 | try { 11 | func = context[func]; 12 | if (typeof func == 'undefined') { 13 | throw `Function ${functionName} is not found in the context ${context}` 14 | } 15 | 16 | } catch (err) { 17 | console.error(`Function ${functionName} is not found in the context ${context}`, err) 18 | } 19 | return func.apply(context, args); 20 | } 21 | 22 | function getObjFromArray(objList, obj_key, key_value, failToFirst) { 23 | failToFirst = typeof (failToFirst) !== 'undefined'; 24 | if (key_value !== '') { 25 | for (let i = 0; i < objList.length; i++) { 26 | if (objList[i][obj_key] === key_value) { 27 | return objList[i]; 28 | } 29 | } 30 | } 31 | if (failToFirst && objList.length > 0) { 32 | return objList[0] 33 | } 34 | 35 | return false; 36 | } 37 | 38 | function calculateTotalOnObjectArray(data, columns) { 39 | // Compute totals in array of objects 40 | // example : 41 | // calculateTotalOnObjectArray ([{ value1:500, value2: 70} , {value:200, value2:15} ], ['value']) 42 | // return {'value1': 700, value2:85} 43 | 44 | let total_container = {}; 45 | for (let r = 0; r < data.length; r++) { 46 | 47 | for (let i = 0; i < columns.length; i++) { 48 | if (typeof total_container[columns[i]] == 'undefined') { 49 | total_container[columns[i]] = 0; 50 | } 51 | let val = data[r][columns[i]]; 52 | if (val === '-') val = 0; 53 | 54 | else if (typeof (val) == 'string') { 55 | try { 56 | val = val.replace(/,/g, ''); 57 | } catch (err) { 58 | console.log(err, val, typeof (val)); 59 | } 60 | } 61 | total_container[columns[i]] += parseFloat(val); 62 | } 63 | } 64 | return total_container; 65 | } 66 | 67 | function get_xpath($element, forceTree) { 68 | if ($element.length === 0) { 69 | return null; 70 | } 71 | 72 | let element = $element[0]; 73 | 74 | if ($element.attr('id') && ((forceTree === undefined) || !forceTree)) { 75 | return '//*[@id="' + $element.attr('id') + '"]'; 76 | } else { 77 | let paths = []; 78 | for (; element && element.nodeType === Node.ELEMENT_NODE; element = element.parentNode) { 79 | let index = 0; 80 | for (let sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) { 81 | if (sibling.nodeType === Node.DOCUMENT_TYPE_NODE) 82 | continue; 83 | if (sibling.nodeName === element.nodeName) 84 | ++index; 85 | } 86 | 87 | var tagName = element.nodeName.toLowerCase(); 88 | var pathIndex = (index ? '[' + (index + 1) + ']' : ''); 89 | paths.splice(0, 0, tagName + pathIndex); 90 | } 91 | 92 | return paths.length ? '/' + paths.join('/') : null; 93 | } 94 | } 95 | 96 | 97 | $.slick_reporting = { 98 | 'getObjFromArray': getObjFromArray, 99 | 'calculateTotalOnObjectArray': calculateTotalOnObjectArray, 100 | "executeFunctionByName": executeFunctionByName, 101 | "get_xpath": get_xpath, 102 | defaults: { 103 | total_label: 'Total', 104 | } 105 | 106 | } 107 | $.slick_reporting.cache = {} 108 | 109 | }(jQuery)); -------------------------------------------------------------------------------- /demo_proj/demo_proj/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo_proj project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "django-insecure-kb+5wbkzz-dxvmzs%49y07g7zkk9@30w%+u@2@d5x!)daivk&7" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "demo_app", 41 | "crispy_forms", 42 | "crispy_bootstrap5", 43 | "slick_reporting", 44 | # "slick_reporting.dashboards", 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | "django.middleware.security.SecurityMiddleware", 49 | "django.contrib.sessions.middleware.SessionMiddleware", 50 | "django.middleware.common.CommonMiddleware", 51 | "django.middleware.csrf.CsrfViewMiddleware", 52 | "django.contrib.auth.middleware.AuthenticationMiddleware", 53 | "django.contrib.messages.middleware.MessageMiddleware", 54 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 55 | ] 56 | 57 | ROOT_URLCONF = "demo_proj.urls" 58 | 59 | TEMPLATES = [ 60 | { 61 | "BACKEND": "django.template.backends.django.DjangoTemplates", 62 | "DIRS": [os.path.join(BASE_DIR, "templates")], 63 | "APP_DIRS": True, 64 | "OPTIONS": { 65 | "context_processors": [ 66 | "django.template.context_processors.debug", 67 | "django.template.context_processors.request", 68 | "django.contrib.auth.context_processors.auth", 69 | "django.contrib.messages.context_processors.messages", 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = "demo_proj.wsgi.application" 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 80 | 81 | DATABASES = { 82 | "default": { 83 | "ENGINE": "django.db.backends.sqlite3", 84 | "NAME": BASE_DIR / "db.sqlite3", 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 101 | }, 102 | { 103 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 110 | 111 | LANGUAGE_CODE = "en-us" 112 | 113 | TIME_ZONE = "UTC" 114 | 115 | USE_I18N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 122 | 123 | STATIC_URL = "static/" 124 | 125 | # Default primary key field type 126 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 127 | 128 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 129 | 130 | CRISPY_TEMPLATE_PACK = "bootstrap5" 131 | CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" 132 | 133 | SLICK_REPORTING_SETTINGS = { 134 | "CHARTS": { 135 | "apexcharts": { 136 | "entryPoint": "DisplayApexPieChart", 137 | "js": ("https://cdn.jsdelivr.net/npm/apexcharts", "slick_reporting/slick_reporting.chartsjs.js"), 138 | "css": {"all": ("https://cdn.jsdelivr.net/npm/apexcharts/dist/apexcharts.min.css",)}, 139 | }, 140 | }, 141 | } 142 | -------------------------------------------------------------------------------- /docs/source/ref/settings.rst: -------------------------------------------------------------------------------- 1 | .. _settings: 2 | 3 | 4 | Settings 5 | ======== 6 | 7 | .. note:: 8 | 9 | Settings are changed in version 1.1.1 to being a dictionary instead of individual variables. 10 | Variables will continue to work till next major release. 11 | 12 | 13 | Below are the default settings for django-slick-reporting. You can override them in your settings file. 14 | 15 | .. code-block:: python 16 | 17 | SLICK_REPORTING_SETTINGS = { 18 | "JQUERY_URL": "https://code.jquery.com/jquery-3.7.0.min.js", 19 | "DEFAULT_START_DATE_TIME": datetime( 20 | datetime.now().year, 1, 1, 0, 0, 0, tzinfo=timezone.utc 21 | ), # Default: 1st Jan of current year 22 | "DEFAULT_END_DATE_TIME": datetime.datetime.today(), # Default to today 23 | "DEFAULT_CHARTS_ENGINE": SLICK_REPORTING_DEFAULT_CHARTS_ENGINE, 24 | "MEDIA": { 25 | "override": False, # set it to True to override the media files, 26 | # False will append the media files to the existing ones. 27 | "js": ( 28 | "https://cdn.jsdelivr.net/momentjs/latest/moment.min.js", 29 | "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js", 30 | "https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js", 31 | "https://cdn.datatables.net/1.13.4/js/dataTables.bootstrap5.min.js", 32 | "slick_reporting/slick_reporting.js", 33 | "slick_reporting/slick_reporting.report_loader.js", 34 | "slick_reporting/slick_reporting.datatable.js", 35 | ), 36 | "css": { 37 | "all": ( 38 | "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css", 39 | "https://cdn.datatables.net/1.13.4/css/dataTables.bootstrap5.min.css", 40 | ) 41 | }, 42 | }, 43 | "FONT_AWESOME": { 44 | "CSS_URL": "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css", 45 | "ICONS": { 46 | "pie": "fas fa-chart-pie", 47 | "bar": "fas fa-chart-bar", 48 | "line": "fas fa-chart-line", 49 | "area": "fas fa-chart-area", 50 | "column": "fas fa-chart-column", 51 | }, 52 | }, 53 | "CHARTS": { 54 | "highcharts": "$.slick_reporting.highcharts.displayChart", 55 | "chartjs": "$.slick_reporting.chartjs.displayChart", 56 | }, 57 | "MESSAGES": { 58 | "total": _("Total"), 59 | }, 60 | } 61 | 62 | * JQUERY_URL: 63 | 64 | Link to the jquery file, You can use set it to False and manage the jQuery addition to your liking 65 | 66 | * DEFAULT_START_DATE_TIME 67 | 68 | Default date time that would appear on the filter form in the start date 69 | 70 | * DEFAULT_END_DATE_TIME 71 | 72 | Default date time that would appear on the filter form in the end date 73 | 74 | * FONT_AWESOME: 75 | 76 | Font awesome is used to display the icon next to the chart title. You can override the following settings: 77 | 78 | 1. ``CSS_URL``: URL to the font-awesome css file 79 | 2. ``ICONS``: Icons used for different chart types. 80 | 81 | * CHARTS: 82 | 83 | The entry points for displaying charts on the front end. 84 | You can add your own chart engine by adding an entry to this dictionary. 85 | 86 | * MESSAGES: 87 | 88 | The strings used in the front end. You can override them here, it also gives a chance to set and translate them per your requirements. 89 | 90 | 91 | Old versions settings: 92 | 93 | 1. ``SLICK_REPORTING_DEFAULT_START_DATE``: Default: the beginning of the current year 94 | 2. ``SLICK_REPORTING_DEFAULT_END_DATE``: Default: the end of the current year. 95 | 3. ``SLICK_REPORTING_FORM_MEDIA``: Controls the media files required by the search form. 96 | Defaults is: 97 | 98 | .. code-block:: python 99 | 100 | SLICK_REPORTING_FORM_MEDIA = { 101 | "css": { 102 | "all": ( 103 | "https://cdn.datatables.net/v/bs4/dt-1.10.20/datatables.min.css", 104 | "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css", 105 | ) 106 | }, 107 | "js": ( 108 | "https://code.jquery.com/jquery-3.3.1.slim.min.js", 109 | "https://cdn.datatables.net/v/bs4/dt-1.10.20/datatables.min.js", 110 | "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js", 111 | "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js", 112 | "https://code.highcharts.com/highcharts.js", 113 | ), 114 | } 115 | 116 | 4. ``SLICK_REPORTING_DEFAULT_CHARTS_ENGINE``: Controls the default chart engine used. 117 | -------------------------------------------------------------------------------- /docs/source/howto/customize_frontend.rst: -------------------------------------------------------------------------------- 1 | Charting and Front End Customization 2 | ===================================== 3 | 4 | 5 | 6 | The ajax response structure 7 | --------------------------- 8 | 9 | Understanding how the response is structured is imperative in order to customize how the report is displayed on the front end 10 | 11 | Let's have a look 12 | 13 | .. code-block:: python 14 | 15 | 16 | # Ajax response or `report_results` template context variable. 17 | response = { 18 | # the report slug, defaults to the class name all lower 19 | "report_slug": "", 20 | # a list of objects representing the actual results of the report 21 | "data": [ 22 | { 23 | "name": "Product 1", 24 | "quantity__sum": "1774", 25 | "value__sum": "8758", 26 | "field_x": "value_x", 27 | }, 28 | { 29 | "name": "Product 2", 30 | "quantity__sum": "1878", 31 | "value__sum": "3000", 32 | "field_x": "value_x", 33 | }, 34 | # etc ..... 35 | ], 36 | # A list explaining the columns/keys in the data results. 37 | # ie: len(response.columns) == len(response.data[i].keys()) 38 | # It contains needed information about verbose name , if summable and hints about the data type. 39 | "columns": [ 40 | { 41 | "name": "name", 42 | "computation_field": "", 43 | "verbose_name": "Name", 44 | "visible": True, 45 | "type": "CharField", 46 | "is_summable": False, 47 | }, 48 | { 49 | "name": "quantity__sum", 50 | "computation_field": "", 51 | "verbose_name": "Quantities Sold", 52 | "visible": True, 53 | "type": "number", 54 | "is_summable": True, 55 | }, 56 | { 57 | "name": "value__sum", 58 | "computation_field": "", 59 | "verbose_name": "Value $", 60 | "visible": True, 61 | "type": "number", 62 | "is_summable": True, 63 | }, 64 | ], 65 | # Contains information about the report as whole if it's time series or a a crosstab 66 | # And what's the actual and verbose names of the time series or crosstab specific columns. 67 | "metadata": { 68 | "time_series_pattern": "", 69 | "time_series_column_names": [], 70 | "time_series_column_verbose_names": [], 71 | "crosstab_model": "", 72 | "crosstab_column_names": [], 73 | "crosstab_column_verbose_names": [], 74 | }, 75 | # A mirror of the set charts_settings on the ReportView 76 | # ``ReportView`` populates the id and the `engine_name' if not set 77 | "chart_settings": [ 78 | { 79 | "type": "pie", 80 | "engine_name": "highcharts", 81 | "data_source": ["quantity__sum"], 82 | "title_source": ["name"], 83 | "title": "Pie Chart (Quantities)", 84 | "id": "pie-0", 85 | }, 86 | { 87 | "type": "bar", 88 | "engine_name": "chartsjs", 89 | "data_source": ["value__sum"], 90 | "title_source": ["name"], 91 | "title": "Column Chart (Values)", 92 | "id": "bar-1", 93 | }, 94 | ], 95 | } 96 | 97 | 98 | The ajax response structure 99 | --------------------------- 100 | 101 | Understanding how the response is structured is imperative in order to customize how the report is displayed on the front end 102 | 103 | Let's have a look 104 | 105 | .. code-block:: python 106 | 107 | 108 | # Ajax response or `report_results` template context variable. 109 | response = { 110 | "report_slug": "", # the report slug, defaults to the class name all lower 111 | "data": [], # a list of objects representing the actual results of the report 112 | "columns": [], # A list explaining the columns/keys in the data results. 113 | # ie: len(response.columns) == len(response.data[i].keys()) 114 | # A List of objects. each object contain field needed information like verbose name , if summable and hints about the data type. 115 | "metadata": {}, # Contains information about the report as whole if it's time series or a a crosstab 116 | # And what's the actual and verbose names of the time series or crosstab specific columns. 117 | "chart_settings": [], # a list of objects mirror of the set charts_settings 118 | } 119 | 120 | 121 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | from slick_reporting.views import ReportView 2 | from slick_reporting.fields import ComputationField, TotalReportField 3 | from django.db.models import Sum, Count 4 | from .models import SimpleSales, ComplexSales, SimpleSales2 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | 8 | class MonthlyProductSales(ReportView): 9 | report_model = SimpleSales 10 | date_field = "doc_date" 11 | group_by = "client" 12 | columns = ["slug", "name"] 13 | time_series_pattern = "monthly" 14 | time_series_columns = ["__total__", "__balance__"] 15 | 16 | 17 | class MonthlyProductSalesToFIeldSet(ReportView): 18 | report_model = SimpleSales2 19 | date_field = "doc_date" 20 | group_by = "client" 21 | columns = ["slug", "name"] 22 | time_series_pattern = "monthly" 23 | time_series_columns = ["__total__", "__balance__"] 24 | 25 | 26 | class ProductClientSalesMatrix(ReportView): 27 | report_title = "awesome report title" 28 | report_model = SimpleSales 29 | date_field = "doc_date" 30 | 31 | group_by = "product" 32 | columns = ["slug", "name"] 33 | 34 | crosstab_field = "client" 35 | crosstab_columns = [TotalReportField] 36 | 37 | chart_settings = [ 38 | { 39 | "type": "pie", 40 | "date_source": "__total__", 41 | "title_source": "__total__", 42 | } 43 | ] 44 | 45 | 46 | class ProductClientSalesMatrixToFieldSet(ReportView): 47 | report_title = "awesome report title" 48 | report_model = SimpleSales2 49 | date_field = "doc_date" 50 | 51 | group_by = "product" 52 | columns = ["slug", "name"] 53 | 54 | crosstab_field = "client" 55 | crosstab_columns = ["__total__"] 56 | 57 | chart_settings = [ 58 | { 59 | "type": "pie", 60 | "date_source": "__total__", 61 | "title_source": "__total__", 62 | } 63 | ] 64 | 65 | 66 | class CrossTabColumnOnFly(ReportView): 67 | report_title = "awesome report title" 68 | report_model = SimpleSales 69 | date_field = "doc_date" 70 | 71 | group_by = "product" 72 | columns = ["slug", "name"] 73 | 74 | crosstab_field = "client" 75 | crosstab_columns = [ 76 | ComputationField.create( 77 | Sum, "value", name="value__sum", verbose_name=_("Sales") 78 | ) 79 | ] 80 | 81 | chart_settings = [ 82 | { 83 | "type": "pie", 84 | "date_source": "value__sum", 85 | "title_source": "name", 86 | } 87 | ] 88 | 89 | 90 | class CrossTabColumnOnFlyToFieldSet(ReportView): 91 | report_title = "awesome report title" 92 | report_model = SimpleSales2 93 | date_field = "doc_date" 94 | 95 | group_by = "product" 96 | columns = ["slug", "name"] 97 | 98 | crosstab_field = "client" 99 | crosstab_columns = [ 100 | ComputationField.create( 101 | Sum, "value", name="value__sum", verbose_name=_("Sales") 102 | ) 103 | ] 104 | 105 | chart_settings = [ 106 | { 107 | "type": "pie", 108 | "date_source": "value__sum", 109 | "title_source": "name", 110 | } 111 | ] 112 | 113 | 114 | class MonthlyProductSalesWQS(ReportView): 115 | queryset = SimpleSales.objects.all() 116 | date_field = "doc_date" 117 | group_by = "client" 118 | columns = ["slug", "name"] 119 | time_series_pattern = "monthly" 120 | time_series_columns = [TotalReportField, "__balance__"] 121 | 122 | 123 | class TaxSales(ReportView): 124 | # report_model = SimpleSales 125 | queryset = ComplexSales.objects.all() 126 | date_field = "doc_date" 127 | group_by = "tax__name" 128 | columns = [ 129 | "tax__name", 130 | ComputationField.create( 131 | Count, "tax", name="tax__count", verbose_name=_("Sales") 132 | ), 133 | ] 134 | chart_settings = [ 135 | { 136 | "type": "pie", 137 | "date_source": "tax__count", 138 | "title_source": "tax__name", 139 | } 140 | ] 141 | 142 | 143 | class MonthlyProductSalesToFIeldSet(ReportView): 144 | report_model = SimpleSales2 145 | date_field = "doc_date" 146 | group_by = "client" 147 | columns = ["slug", "name"] 148 | time_series_pattern = "monthly" 149 | time_series_columns = ["__total__", "__balance__"] 150 | 151 | 152 | class TaxSales(ReportView): 153 | # report_model = SimpleSales 154 | queryset = ComplexSales.objects.all() 155 | date_field = "doc_date" 156 | group_by = "tax__name" 157 | columns = [ 158 | "tax__name", 159 | ComputationField.create( 160 | Count, "tax", name="tax__count", verbose_name=_("Sales") 161 | ), 162 | ] 163 | chart_settings = [ 164 | { 165 | "type": "pie", 166 | "date_source": "tax__count", 167 | "title_source": "tax__name", 168 | } 169 | ] 170 | -------------------------------------------------------------------------------- /docs/source/topics/filter_form.rst: -------------------------------------------------------------------------------- 1 | .. _filter_form: 2 | 3 | Customizing Filter Form 4 | ======================= 5 | 6 | The filter form is a form that is used to filter the data to be used in the report. 7 | 8 | 9 | The generated form 10 | ------------------- 11 | 12 | Behind the scene, The view calls ``slick_reporting.form_factory.report_form_factory`` in ``get_form_class`` method. 13 | ``report_form_factory`` is a helper method which generates a form containing start date and end date, as well as all foreign keys on the report_model. 14 | 15 | Changing the generated form API is still private, however, you can use your own form easily. 16 | 17 | Overriding the Form 18 | -------------------- 19 | 20 | The system expect that the form used with the ``ReportView`` to implement the ``slick_reporting.forms.BaseReportForm`` interface. 21 | 22 | The interface is simple, only 3 mandatory methods to implement, The rest are mandatory only if you are working with a crosstab report or a time series report. 23 | 24 | 25 | * ``get_filters``: Mandatory, return a tuple (Q_filters , kwargs filter) to be used in filtering. 26 | q_filter: can be none or a series of Django's Q queries 27 | kwargs_filter: None or a dictionary of filters 28 | 29 | * ``get_start_date``: Mandatory, return the start date of the report. 30 | 31 | * ``get_end_date``: Mandatory, return the end date of the report. 32 | 33 | * ``get_crispy_helper`` : Optional, return a crispy form helper to be used in rendering the form. 34 | 35 | In case you are working with a crosstab report, you need to implement the following methods: 36 | 37 | * ``get_crosstab_compute_remainder``: return a boolean indicating if the remainder should be computed or not. 38 | 39 | * ``get_crosstab_ids``: return a list of ids to be used in the crosstab report. 40 | 41 | 42 | And in case you are working with a time series report, with a selector on, you need to implement the following method: 43 | 44 | * ``get_time_series_pattern``: return a string representing the time series pattern. ie: ``ie: daily, monthly, yearly`` 45 | 46 | Example a full example of a custom form: 47 | 48 | .. code-block:: python 49 | 50 | # forms.py 51 | from slick_reporting.forms import BaseReportForm 52 | 53 | # A Normal form , Inheriting from BaseReportForm 54 | class RequestLogForm(BaseReportForm, forms.Form): 55 | 56 | SECURE_CHOICES = ( 57 | ("all", "All"), 58 | ("secure", "Secure"), 59 | ("non-secure", "Not Secure"), 60 | ) 61 | 62 | start_date = forms.DateField( 63 | required=False, 64 | label="Start Date", 65 | widget=forms.DateInput(attrs={"type": "date"}), 66 | ) 67 | end_date = forms.DateField( 68 | required=False, label="End Date", widget=forms.DateInput(attrs={"type": "date"}) 69 | ) 70 | secure = forms.ChoiceField( 71 | choices=SECURE_CHOICES, required=False, label="Secure", initial="all" 72 | ) 73 | method = forms.CharField(required=False, label="Method") 74 | response = forms.ChoiceField( 75 | choices=HTTP_STATUS_CODES, 76 | required=False, 77 | label="Response", 78 | initial="200", 79 | ) 80 | other_people_only = forms.BooleanField( 81 | required=False, label="Show requests from other People Only" 82 | ) 83 | 84 | def __init__(self, *args, **kwargs): 85 | super(RequestLogForm, self).__init__(*args, **kwargs) 86 | # provide initial values and ay needed customization 87 | self.fields["start_date"].initial = datetime.date.today() 88 | self.fields["end_date"].initial = datetime.date.today() 89 | 90 | def get_filters(self): 91 | # return the filters to be used in the report 92 | # Note: the use of Q filters and kwargs filters 93 | filters = {} 94 | q_filters = [] 95 | if self.cleaned_data["secure"] == "secure": 96 | filters["is_secure"] = True 97 | elif self.cleaned_data["secure"] == "non-secure": 98 | filters["is_secure"] = False 99 | if self.cleaned_data["method"]: 100 | filters["method"] = self.cleaned_data["method"] 101 | if self.cleaned_data["response"]: 102 | filters["response"] = self.cleaned_data["response"] 103 | if self.cleaned_data["other_people_only"]: 104 | q_filters.append(~Q(user=self.request.user)) 105 | 106 | return q_filters, filters 107 | 108 | def get_start_date(self): 109 | return self.cleaned_data["start_date"] 110 | 111 | def get_end_date(self): 112 | return self.cleaned_data["end_date"] 113 | 114 | # ---- 115 | # in views.py 116 | 117 | from .forms import RequestLogForm 118 | 119 | class RequestCountByPath(ReportView): 120 | form_class = RequestLogForm 121 | 122 | You can view this code snippet in action on the demo project https://django-slick-reporting.com/total-product-sales-with-custom-form/ 123 | -------------------------------------------------------------------------------- /slick_reporting/app_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import get_callable 3 | from django.utils.functional import lazy 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | import datetime 7 | 8 | 9 | def get_first_of_this_year(): 10 | d = datetime.datetime.today() 11 | return datetime.datetime(d.year, 1, 1, 0, 0) 12 | 13 | 14 | def get_end_of_this_year(): 15 | d = datetime.datetime.today() 16 | return datetime.datetime(d.year + 1, 1, 1, 0, 0) 17 | 18 | 19 | def get_start_date(): 20 | start_date = getattr(settings, "SLICK_REPORTING_DEFAULT_START_DATE", False) 21 | return start_date or get_first_of_this_year() 22 | 23 | 24 | def get_end_date(): 25 | end_date = getattr(settings, "SLICK_REPORTING_DEFAULT_END_DATE", False) 26 | return end_date or datetime.datetime.today() 27 | 28 | 29 | SLICK_REPORTING_DEFAULT_START_DATE = lazy(get_start_date, datetime.datetime)() 30 | SLICK_REPORTING_DEFAULT_END_DATE = lazy(get_end_date, datetime.datetime)() 31 | 32 | 33 | SLICK_REPORTING_DEFAULT_CHARTS_ENGINE = getattr(settings, "SLICK_REPORTING_DEFAULT_CHARTS_ENGINE", "highcharts") 34 | 35 | 36 | SLICK_REPORTING_JQUERY_URL = getattr( 37 | settings, "SLICK_REPORTING_JQUERY_URL", "https://code.jquery.com/jquery-3.7.0.min.js" 38 | ) 39 | 40 | 41 | SLICK_REPORTING_SETTINGS_DEFAULT = { 42 | "JQUERY_URL": SLICK_REPORTING_JQUERY_URL, 43 | "DEFAULT_START_DATE_TIME": get_start_date(), 44 | "DEFAULT_END_DATE_TIME": get_end_date(), 45 | "DEFAULT_CHARTS_ENGINE": SLICK_REPORTING_DEFAULT_CHARTS_ENGINE, 46 | "MEDIA": { 47 | "override": False, 48 | "js": ( 49 | "https://cdn.jsdelivr.net/momentjs/latest/moment.min.js", 50 | "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js", 51 | "https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js", 52 | "https://cdn.datatables.net/1.13.4/js/dataTables.bootstrap5.min.js", 53 | "slick_reporting/slick_reporting.js", 54 | "slick_reporting/slick_reporting.report_loader.js", 55 | "slick_reporting/slick_reporting.datatable.js", 56 | ), 57 | "css": { 58 | "all": ( 59 | "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css", 60 | "https://cdn.datatables.net/1.13.4/css/dataTables.bootstrap5.min.css", 61 | ) 62 | }, 63 | }, 64 | "FONT_AWESOME": { 65 | "CSS_URL": "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css", 66 | "ICONS": { 67 | "pie": "fas fa-chart-pie", 68 | "bar": "fas fa-chart-bar", 69 | "line": "fas fa-chart-line", 70 | "area": "fas fa-chart-area", 71 | "column": "fas fa-chart-bar", 72 | }, 73 | }, 74 | "CHARTS": { 75 | "highcharts": { 76 | "entryPoint": "$.slick_reporting.highcharts.displayChart", 77 | "js": ("https://code.highcharts.com/highcharts.js", "slick_reporting/slick_reporting.highchart.js"), 78 | }, 79 | "chartsjs": { 80 | "entryPoint": "$.slick_reporting.chartsjs.displayChart", 81 | "js": ("https://cdn.jsdelivr.net/npm/chart.js", "slick_reporting/slick_reporting.chartsjs.js"), 82 | }, 83 | }, 84 | "MESSAGES": { 85 | "total": _("Total"), 86 | "export_to_csv": _("Export to CSV"), 87 | }, 88 | "REPORT_VIEW_ACCESS_FUNCTION": "slick_reporting.helpers.user_test_function", 89 | } 90 | 91 | 92 | def get_slick_reporting_settings(): 93 | slick_settings = SLICK_REPORTING_SETTINGS_DEFAULT.copy() 94 | slick_chart_settings = slick_settings["CHARTS"].copy() 95 | 96 | user_settings = getattr(settings, "SLICK_REPORTING_SETTINGS", {}) 97 | user_chart_settings = user_settings.get("CHARTS", {}) 98 | 99 | user_media_settings = user_settings.get("MEDIA", {}) 100 | override_media = user_media_settings.get("override", False) 101 | if override_media: 102 | slick_settings["MEDIA"] = user_media_settings 103 | else: 104 | slick_settings["MEDIA"]["js"] = slick_settings["MEDIA"]["js"] + user_media_settings.get("js", ()) 105 | slick_settings["MEDIA"]["css"]["all"] = slick_settings["MEDIA"]["css"]["all"] + user_media_settings.get( 106 | "css", {} 107 | ).get("all", ()) 108 | 109 | slick_chart_settings.update(user_chart_settings) 110 | slick_settings.update(user_settings) 111 | slick_settings["CHARTS"] = slick_chart_settings 112 | 113 | # slick_settings = {**SLICK_REPORTING_SETTINGS_DEFAULT, **getattr(settings, "SLICK_REPORTING_SETTINGS", {})} 114 | start_date = getattr(settings, "SLICK_REPORTING_DEFAULT_START_DATE", False) 115 | end_date = getattr(settings, "SLICK_REPORTING_DEFAULT_END_DATE", False) 116 | # backward compatibility, todo remove in next major release 117 | if start_date: 118 | slick_settings["DEFAULT_START_DATE_TIME"] = start_date 119 | if end_date: 120 | slick_settings["DEFAULT_END_DATE_TIME"] = end_date 121 | 122 | return slick_settings 123 | 124 | 125 | SLICK_REPORTING_SETTINGS = lazy(get_slick_reporting_settings, dict)() 126 | 127 | 128 | def get_media(): 129 | return SLICK_REPORTING_SETTINGS["MEDIA"] 130 | 131 | 132 | def get_access_function(): 133 | return get_callable(SLICK_REPORTING_SETTINGS["REPORT_VIEW_ACCESS_FUNCTION"]) 134 | -------------------------------------------------------------------------------- /docs/source/howto/index.rst: -------------------------------------------------------------------------------- 1 | .. _how_to: 2 | 3 | ======= 4 | How To 5 | ======= 6 | In this section we will go over some of the frequent tasks you will need to do when using ReportView. 7 | 8 | 9 | Customize the form 10 | ================== 11 | 12 | The filter form is automatically generated for convenience 13 | but you can override it and add your own Form. 14 | 15 | The system expect that the form used with the ``ReportView`` to implement the ``slick_reporting.forms.BaseReportForm`` interface. 16 | The interface is simple, only 3 mandatory methods to implement, The rest are mandatory only if you are working with a crosstab report or a time series report. 17 | 18 | #. get_filters: return the filters to be used in the report in a tuple 19 | The first element is a list of Q filters (is any) 20 | The second element is a dict of filters to be used in the queryset 21 | These filters will be passed to the report_model.objects.filter(*q_filters, **kw_filters) 22 | 23 | #. get_start_date: return the start date to be used in the report 24 | 25 | #. get_end_date: return the end date to be used in the report 26 | 27 | 28 | 29 | .. code-block:: python 30 | 31 | # forms.py 32 | from slick_reporting.forms import BaseReportForm 33 | 34 | 35 | class RequestFilterForm(BaseReportForm, forms.Form): 36 | 37 | SECURE_CHOICES = ( 38 | ("all", "All"), 39 | ("secure", "Secure"), 40 | ("non-secure", "Not Secure"), 41 | ) 42 | 43 | start_date = forms.DateField( 44 | required=False, 45 | label="Start Date", 46 | widget=forms.DateInput(attrs={"type": "date"}), 47 | ) 48 | end_date = forms.DateField( 49 | required=False, label="End Date", widget=forms.DateInput(attrs={"type": "date"}) 50 | ) 51 | secure = forms.ChoiceField( 52 | choices=SECURE_CHOICES, required=False, label="Secure", initial="all" 53 | ) 54 | method = forms.CharField(required=False, label="Method") 55 | 56 | other_people_only = forms.BooleanField( 57 | required=False, label="Show requests from other People Only" 58 | ) 59 | 60 | def __init__(self, request=None, *args, **kwargs): 61 | self.request = request 62 | super().__init__(*args, **kwargs) 63 | self.fields["start_date"].initial = datetime.date.today() 64 | self.fields["end_date"].initial = datetime.date.today() 65 | 66 | def get_filters(self): 67 | q_filters = [] 68 | kw_filters = {} 69 | 70 | if self.cleaned_data["secure"] == "secure": 71 | kw_filters["is_secure"] = True 72 | elif self.cleaned_data["secure"] == "non-secure": 73 | kw_filters["is_secure"] = False 74 | if self.cleaned_data["method"]: 75 | kw_filters["method"] = self.cleaned_data["method"] 76 | if self.cleaned_data["response"]: 77 | kw_filters["response"] = self.cleaned_data["response"] 78 | if self.cleaned_data["other_people_only"]: 79 | q_filters.append(~Q(user=self.request.user)) 80 | 81 | return q_filters, kw_filters 82 | 83 | def get_start_date(self): 84 | return self.cleaned_data["start_date"] 85 | 86 | def get_end_date(self): 87 | return self.cleaned_data["end_date"] 88 | 89 | For a complete reference of the ``BaseReportForm`` interface, check :ref:`filter_form_customization` 90 | 91 | 92 | Use the report view in our own template 93 | --------------------------------------- 94 | To use the report template with your own project templates, you simply need to override the ``slick_reporting/base.html`` template to make it extends your own base template 95 | You only need to have a ``{% block content %}`` in your base template to be able to use the report template 96 | and a ``{% block extrajs %}`` block to add the javascript implementation. 97 | 98 | 99 | The example below assumes you have a ``base.html`` template in your project templates folder and have a content block and a project_extrajs block in it. 100 | 101 | .. code-block:: html 102 | 103 | {% extends "base.html" %} 104 | {% load static %} 105 | 106 | {% block content %} 107 | 108 | {% endblock %} 109 | 110 | {% block project_extrajs %} 111 | {% include "slick_reporting/js_resources.html" %} 112 | {% block extrajs %} 113 | {% endblock %} 114 | 115 | {% endblock %} 116 | 117 | 118 | Work with tree data & Nested categories 119 | --------------------------------------- 120 | 121 | 122 | 123 | 124 | 125 | Change the report structure in response to User input 126 | ----------------------------------------------------- 127 | 128 | 129 | Create your own Chart Engine 130 | ----------------------------- 131 | 132 | Create a Custom ComputationField and reuse it 133 | --------------------------------------------- 134 | 135 | 136 | 137 | Add a new chart engine 138 | ---------------------- 139 | 140 | 141 | Add an exporting option 142 | ----------------------- 143 | 144 | 145 | 146 | Work with categorical data 147 | -------------------------- 148 | 149 | How to create a custom ComputationField 150 | --------------------------------------- 151 | 152 | 153 | create custom columns 154 | --------------------- 155 | 156 | 157 | format numbers in the datatable 158 | 159 | 160 | custom group by 161 | custom time series periods 162 | custom crosstab reports 163 | 164 | .. toctree:: 165 | :maxdepth: 2 166 | :caption: Topics: 167 | :titlesonly: 168 | 169 | 170 | customize_frontend 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/source/topics/integrating_slick_reporting.rst: -------------------------------------------------------------------------------- 1 | Integrating reports into your front end 2 | ======================================= 3 | 4 | To integrate Slick Reporting into your application, you need to do override "slick_reporting/base.html" template, 5 | and/or, for more fine control over the report layout, override "slick_reporting/report.html" template. 6 | 7 | Example 1: Override base.html 8 | 9 | .. code-block:: html+django 10 | 11 | {% extends "base.html" %} 12 | 13 | {% block meta_page_title %} {{ report_title }}{% endblock %} 14 | {% block page_title %} {{ report_title }} {% endblock %} 15 | 16 | {% block extrajs %} 17 | {{ block.super }} 18 | {% include "slick_reporting/js_resources.html" %} 19 | {% endblock %} 20 | 21 | 22 | 23 | Let's see what we did there 24 | 1. We made our slick_reporting/base.html extend the main base.html 25 | 2. We added the ``report_title`` context variable (which hold the current report title) to the meta_page_title and page_title blocks. 26 | Use your version of these blocks, you might have them named differently. 27 | 3. We added the slick_reporting/js_resources.html template to the extrajs block. This template contains the javascript resources needed for slick_reporting to work. 28 | Also, use your version of the extrajs block. You might have it named differently. 29 | 30 | And that's it ! You can now use slick_reporting in your application. 31 | 32 | 33 | Example 2: Override report.html 34 | 35 | Maybe you want to add some extra information to the report, or change the layout of the report. 36 | You can do this by overriding the slick_reporting/report.html template. 37 | 38 | Here is how it looks like: 39 | 40 | .. code-block:: html+django 41 | 42 | {% extends 'slick_reporting/base.html' %} 43 | {% load crispy_forms_tags i18n %} 44 | 45 | {% block content %} 46 |
47 | {% if form %} 48 |
49 |
50 |

{% trans "Filters" %}

51 |
52 |
53 | {% crispy form crispy_helper %} 54 |
55 | 60 |
61 | {% endif %} 62 | 63 |
64 |
65 |
{% trans "Results" %}
66 |
67 |
68 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | {% endblock %} 83 | 84 | 85 | Integrating reports into your Admin site 86 | ========================================= 87 | 88 | 1. Most probably you would want to override the default admin to add the extra report urls 89 | https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#overriding-the-default-admin-site 90 | 91 | 2. Add the report url to your admin site main get_urls 92 | 93 | .. code-block:: python 94 | 95 | class CustomAdminAdminSite(admin.AdminSite): 96 | def get_urls(self): 97 | from my_apps.reports import MyAwesomeReport 98 | 99 | urls = super().get_urls() 100 | urls = [ 101 | path( 102 | "reports/my-awesome-report/", 103 | MyAwesomeReport.as_view(), 104 | name="my-awesome-report", 105 | ), 106 | ] + urls 107 | return urls 108 | 109 | Note that you need to add the reports urls to the top, or else the wildcard catch will raise a 404 110 | 111 | 3. Override slick_reporting/base.html to extend the admin site 112 | 113 | .. code-block:: html+django 114 | 115 | {% extends 'admin/base_site.html' %} 116 | {% load i18n static slick_reporting_tags %} 117 | 118 | {% block title %}{{ report_title }}{% endblock %} 119 | 120 | {% block extrahead %} 121 | {% include "slick_reporting/js_resources.html" %} 122 | {% get_charts_media "all" %} 123 | {% endblock %} 124 | 125 | {% block breadcrumbs %} 126 | 132 | {% endblock %} 133 | 134 | 135 | 4. You might want to override the report.html as well to set your styles, You can also use a different template for the crispy form 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/source/topics/crosstab_options.rst: -------------------------------------------------------------------------------- 1 | .. _crosstab_reports: 2 | 3 | Crosstab Reports 4 | ================= 5 | Use crosstab reports, also known as matrix reports, to show the relationships between three or more query items. 6 | Crosstab reports show data in rows and columns with information summarized at the intersection points. 7 | 8 | 9 | General use case 10 | ---------------- 11 | Here is a general use case: 12 | 13 | .. code-block:: python 14 | 15 | from django.utils.translation import gettext_lazy as _ 16 | from django.db.models import Sum 17 | from slick_reporting.views import ReportView 18 | 19 | 20 | class CrosstabReport(ReportView): 21 | report_title = _("Cross tab Report") 22 | report_model = SalesTransaction 23 | group_by = "client" 24 | date_field = "date" 25 | 26 | columns = [ 27 | "name", 28 | "__crosstab__", 29 | # You can customize where the crosstab columns are displayed in relation to the other columns 30 | ComputationField.create(Sum, "value", verbose_name=_("Total Value")), 31 | # This is the same as the calculation in the crosstab, 32 | # but this one will be on the whole set. IE total value. 33 | ] 34 | 35 | crosstab_field = "product" 36 | crosstab_columns = [ 37 | ComputationField.create(Sum, "value", verbose_name=_("Value")), 38 | ] 39 | 40 | Crosstab on a Traversing Field 41 | ------------------------------ 42 | You can also crosstab on a traversing field. In the example below we extend the previous crosstab report to be on the product sizes 43 | 44 | .. code-block:: python 45 | 46 | class CrosstabWithTraversingField(CrosstabReport): 47 | crosstab_field = "product__size" 48 | 49 | 50 | Customizing the crosstab ids 51 | ---------------------------- 52 | You can set the default ids that you want to crosstab on, so the initial report, ie without user setting anything, comes out with the values you want 53 | 54 | .. code-block:: python 55 | 56 | class CrosstabWithIds(CrosstabReport): 57 | def get_crosstab_ids(self): 58 | return [Product.objects.first().pk, Product.objects.last().pk] 59 | 60 | 61 | Customizing the Crosstab Filter 62 | ------------------------------- 63 | 64 | For more fine tuned report, You can customize the crosstab report by supplying a list of tuples to the ``crosstab_ids_custom_filters`` attribute. 65 | The tuple should have 2 items, the first is a list of Q object(s) -if any- , and the second is a dict of kwargs filters . Both will be passed to the filter method of the ``report_model``. 66 | 67 | Example: 68 | 69 | .. code-block:: python 70 | 71 | class CrosstabWithIdsCustomFilter(CrosstabReport): 72 | crosstab_ids_custom_filters = [ 73 | (~Q(product__size__in=["extra_big", "big"]), dict()), 74 | (None, dict(product__size__in=["extra_big", "big"])), 75 | ] 76 | # Note: 77 | # if crosstab_ids_custom_filters is set, these settings has NO EFFECT 78 | # crosstab_field = "client" 79 | # crosstab_ids = [1, 2] 80 | # crosstab_compute_remainder = True 81 | 82 | 83 | 84 | Customizing the verbose name of the crosstab columns 85 | ---------------------------------------------------- 86 | Similar to what we did in customizing the verbose name of the computation field for the time series, 87 | Here, We also can customize the verbose name of the crosstab columns by Subclass ``ComputationField`` and setting the ``crosstab_field_verbose_name`` attribute on your custom class. 88 | Default is that the verbose name will display the id of the crosstab field, and the remainder column will be called "The remainder". 89 | 90 | Let's see two examples on how we can customize the verbose name. 91 | 92 | Example 1: On a "regular" crosstab report 93 | 94 | .. code-block:: python 95 | 96 | class CustomCrossTabTotalField(ComputationField): 97 | calculation_field = "value" 98 | calculation_method = Sum 99 | verbose_name = _("Sales for") 100 | name = "sum__value" 101 | 102 | @classmethod 103 | def get_crosstab_field_verbose_name(cls, model, id): 104 | if id == "----": # 4 dashes: the remainder column 105 | return _("Rest of Products") 106 | 107 | name = Product.objects.get(pk=id).name 108 | return f"{cls.verbose_name} {name}" 109 | 110 | 111 | class CrossTabReportWithCustomVerboseName(CrosstabReport): 112 | crosstab_columns = [CustomCrossTabTotalField] 113 | 114 | Example 2: On the ``crosstab_ids_custom_filters`` one 115 | 116 | .. code-block:: python 117 | 118 | class CustomCrossTabTotalField2(CustomCrossTabTotalField): 119 | @classmethod 120 | def get_crosstab_field_verbose_name(cls, model, id): 121 | if id == 0: 122 | return f"{cls.verbose_name} Big and Extra Big" 123 | return f"{cls.verbose_name} all other sizes" 124 | 125 | 126 | class CrossTabReportWithCustomVerboseNameCustomFilter(CrosstabWithIdsCustomFilter): 127 | crosstab_columns = [CustomCrossTabTotalField2] 128 | 129 | 130 | 131 | Example 132 | ------- 133 | 134 | .. image:: _static/crosstab.png 135 | :width: 800 136 | :alt: crosstab 137 | :align: center 138 | 139 | 140 | 1. The Group By. In this example, it is the product field. 141 | 2. The Crosstab. In this example, it is the client field. crosstab_ids were set to client 1 and client 2 142 | 3. The remainder. In this example, it is the rest of the clients. crosstab_compute_remainder was set to True 143 | -------------------------------------------------------------------------------- /docs/source/topics/charts.rst: -------------------------------------------------------------------------------- 1 | Charts Customization 2 | ==================== 3 | 4 | Charts Configuration 5 | --------------------- 6 | 7 | ReportView ``charts_settings`` is a list of objects which each object represent a chart configurations. 8 | The chart configurations are: 9 | 10 | * title: the Chart title. Defaults to the `report_title`. 11 | * type: A string. Examples are pie, bar, line, etc ... 12 | * engine_name: A string, default to the ReportView ``chart_engine`` attribute, then to the ``SLICK_REPORTING_SETTINGS.DEFAULT_CHARTS_ENGINE``. 13 | * data_source: string, the field name containing the numbers we want to plot. 14 | * title_source: string, the field name containing labels of the data_source 15 | * plot_total: if True the chart will plot the total of the columns. Useful with time series and crosstab reports. 16 | * entryPoint: the javascript entry point to display the chart, the entryPoint function accepts the data, $elem and the chartSettings parameters. 17 | 18 | On front end, for each chart needed we pass the whole response to the relevant chart helper function and it handles the rest. 19 | 20 | 21 | 22 | Customizing the entryPoint for a chart 23 | -------------------------------------- 24 | 25 | Sometimes you want to display the chart differently, in this case, you can just change the entryPoint function. 26 | 27 | Example: 28 | 29 | .. code-block:: python 30 | 31 | class ProductSalesApexChart(ReportView): 32 | # .. 33 | template_name = "product_sales_report.html" 34 | chart_settings = [ 35 | # .. 36 | Chart( 37 | "Total sold $", 38 | type="bar", 39 | data_source=["value__sum"], 40 | title_source=["name"], 41 | entryPoint="displayChartCustomEntryPoint", # this is the new entryPoint 42 | ), 43 | ] 44 | 45 | 46 | Then in your template `product_sales_report.html` add the javascript function specified as the new entryPoint. 47 | 48 | .. code-block:: html+django 49 | 50 | {% extends "slick_reporting/report.html" %} 51 | {% load slick_reporting_tags %} 52 | {% block extra_js %} 53 | {{ block.super }} 54 | 62 | 63 | {% endblock %} 64 | 65 | Adding a new charting engine 66 | ---------------------------- 67 | 68 | In the following part we will add some Apex charts to the demo app to demonstrate how you can add your own charting engine to slick reporting. 69 | 70 | #. We need to add the new chart Engine to the settings. Note that the css and js are specified and handled like Django's ``Form.Media`` 71 | 72 | .. code-block:: python 73 | 74 | SLICK_REPORTING_SETTINGS = { 75 | "CHARTS": { 76 | "apexcharts": { 77 | "entryPoint": "DisplayApexPieChart", 78 | "js": ( 79 | "https://cdn.jsdelivr.net/npm/apexcharts", 80 | "js_file_for_apex_chart.js", # this file contains the entryPoint function and is responsible 81 | # for compiling the data and rendering the chart 82 | ), 83 | "css": { 84 | "all": "https://cdn.jsdelivr.net/npm/apexcharts/dist/apexcharts.min.css" 85 | }, 86 | } 87 | }, 88 | } 89 | 90 | #. Add the entry point function to the javascript file `js_file_for_apex_chart.js` in this example. 91 | 92 | It can look something like this: 93 | 94 | .. code-block:: javascript 95 | 96 | let chart = null; 97 | function DisplayApexPieChart(data, $elem, chartOptions) { 98 | // Where: 99 | // data: is the ajax response coming from server 100 | // $elem: is the jquery element where the chart should be rendered 101 | // chartOptions: is the relevant chart dictionary/object in your ReportView chart_settings 102 | 103 | let legendAndSeries = $.slick_reporting.chartsjs.getGroupByLabelAndSeries(data, chartOptions); 104 | // `getGroupByLabelAndSeries` is a helper function that will return an object with two keys: labels and series 105 | 106 | let options = {} 107 | if (chartOptions.type === "pie") { 108 | options = { 109 | series: legendAndSeries.series, 110 | chart: { 111 | type: "pie", 112 | height: 350 113 | }, 114 | labels: legendAndSeries.labels, 115 | }; 116 | } else { 117 | options = { 118 | chart: { 119 | type: 'bar' 120 | }, 121 | series: [{ 122 | name: 'Sales', 123 | data: legendAndSeries.series 124 | }], 125 | xaxis: { 126 | categories: legendAndSeries.labels, 127 | } 128 | } 129 | } 130 | 131 | try { 132 | // destroy old chart, if any 133 | chart.destroy(); 134 | } catch (e) { 135 | // do nothing 136 | } 137 | 138 | chart = new ApexCharts($elem[0], options); 139 | chart.render(); 140 | } 141 | 142 | -------------------------------------------------------------------------------- /docs/source/topics/group_by_report.rst: -------------------------------------------------------------------------------- 1 | .. _group_by_topic: 2 | 3 | ================ 4 | Group By Reports 5 | ================ 6 | 7 | General use case 8 | ---------------- 9 | 10 | Group by reports are reports that group the data by a specific field, while doing some kind of calculation on the grouped fields. For example, a report that groups the expenses by the expense type. 11 | 12 | 13 | Example: 14 | 15 | .. code-block:: python 16 | 17 | class GroupByReport(ReportView): 18 | report_model = SalesTransaction 19 | report_title = _("Group By Report") 20 | date_field = "date" 21 | group_by = "product" 22 | 23 | columns = [ 24 | "name", 25 | ComputationField.create( 26 | method=Sum, 27 | field="value", 28 | name="value__sum", 29 | verbose_name="Total sold $", 30 | is_summable=True, 31 | ), 32 | ] 33 | 34 | # Charts 35 | chart_settings = [ 36 | Chart( 37 | "Total sold $", 38 | Chart.BAR, 39 | data_source=["value__sum"], 40 | title_source=["name"], 41 | ), 42 | ] 43 | 44 | 45 | A Sample group by report would look like this: 46 | 47 | .. image:: _static/group_report.png 48 | :width: 800 49 | :alt: Group Report 50 | :align: center 51 | 52 | In the columns you can access to fields on the model that is being grouped by, in this case the Expense model, and the computation fields. 53 | 54 | Group by a traversing field 55 | --------------------------- 56 | 57 | ``group_by`` value can be a traversing field. If set, the report will be grouped by the last field in the traversing path, 58 | and, the columns available will be those of the last model in the traversing path. 59 | 60 | 61 | Example: 62 | 63 | .. code-block:: python 64 | 65 | # Inherit from previous report and make another version, keeping the columns and charts 66 | class GroupByTraversingFieldReport(GroupByReport): 67 | 68 | report_title = _("Group By Traversing Field") 69 | group_by = "product__product_category" # Note the traversing 70 | 71 | 72 | .. _group_by_custom_querysets_topic: 73 | 74 | Group by custom querysets 75 | ------------------------- 76 | 77 | Grouping can also be over a curated queryset(s). 78 | 79 | Example: 80 | 81 | .. code-block:: python 82 | 83 | class GroupByCustomQueryset(ReportView): 84 | report_model = SalesTransaction 85 | report_title = _("Group By Custom Queryset") 86 | date_field = "date" 87 | 88 | group_by_custom_querysets = [ 89 | SalesTransaction.objects.filter(product__size__in=["big", "extra_big"]), 90 | SalesTransaction.objects.filter(product__size__in=["small", "extra_small"]), 91 | SalesTransaction.objects.filter(product__size="medium"), 92 | ] 93 | group_by_custom_querysets_column_verbose_name = _("Product Size") 94 | 95 | columns = [ 96 | "__index__", 97 | ComputationField.create( 98 | Sum, "value", verbose_name=_("Total Sold $"), name="value" 99 | ), 100 | ] 101 | 102 | chart_settings = [ 103 | Chart( 104 | title="Total sold By Size $", 105 | type=Chart.PIE, 106 | data_source=["value"], 107 | title_source=["__index__"], 108 | ), 109 | Chart( 110 | title="Total sold By Size $", 111 | type=Chart.BAR, 112 | data_source=["value"], 113 | title_source=["__index__"], 114 | ), 115 | ] 116 | 117 | def format_row(self, row_obj): 118 | # Put the verbose names we need instead of the integer index 119 | index = row_obj["__index__"] 120 | if index == 0: 121 | row_obj["__index__"] = "Big" 122 | elif index == 1: 123 | row_obj["__index__"] = "Small" 124 | elif index == 2: 125 | row_obj["__index__"] = "Medium" 126 | return row_obj 127 | 128 | 129 | This report will create two groups, one for pending sales and another for paid and overdue together. 130 | 131 | The ``__index__`` column is a "magic" column, it will added automatically to the report if it's not added. 132 | It just hold the index of the row in the group. 133 | its verbose name (ie the one on the table header) can be customized via ``group_by_custom_querysets_column_verbose_name`` 134 | 135 | You can then customize the *value* of the __index__ column via ``format_row`` hook 136 | 137 | .. _no_group_by_topic: 138 | 139 | The No Group By 140 | --------------- 141 | Sometimes you want to get some calculations done on the whole report_model, without a group_by. 142 | You can do that by having the calculation fields you need in the columns, and leave out the group by. 143 | 144 | Example: 145 | 146 | .. code-block:: python 147 | 148 | class NoGroupByReport(ReportView): 149 | report_model = SalesTransaction 150 | report_title = _("No-Group-By Report [WIP]") 151 | date_field = "date" 152 | group_by = "" 153 | 154 | columns = [ 155 | ComputationField.create( 156 | method=Sum, 157 | field="value", 158 | name="value__sum", 159 | verbose_name="Total sold $", 160 | is_summable=True, 161 | ), 162 | ] 163 | 164 | This report will give one number, the sum of all the values in the ``value`` field of the ``SalesTransaction`` model, within a period. 165 | -------------------------------------------------------------------------------- /slick_reporting/static/slick_reporting/slick_reporting.datatable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by ramez on 2/5/15. 3 | * A wrapper around Datatables.net 4 | * 5 | */ 6 | 7 | 8 | (function ($) { 9 | let _cache = {}; 10 | let _instances = {} 11 | 12 | 13 | function constructTable(css_class, cols, cols_names, add_footer, total_verbose, total_fields, data) { 14 | // Construct an HTML table , header and footer , without a body as it is filled by th datatable.net plugin 15 | cols = typeof cols != 'undefined' ? cols : false; 16 | cols_names = typeof cols_names != 'undefined' ? cols_names : cols; 17 | 18 | let return_val = ``; 19 | let header_th = ''; 20 | let footer_th = ''; 21 | let footer_colspan = 0; 22 | let stop_colspan_detection = false; 23 | let totals_container = $.slick_reporting.calculateTotalOnObjectArray(data, total_fields); 24 | if (data.length <= 1) { 25 | add_footer = false; 26 | } 27 | 28 | for (let i = 0; i < cols.length; i++) { 29 | let col_name = cols[i].name; 30 | header_th += ``; 31 | if (total_fields.indexOf(col_name) !== -1) { 32 | stop_colspan_detection = true; 33 | } 34 | if (!stop_colspan_detection) { 35 | footer_colspan += 1; 36 | } else { 37 | let column_total = totals_container[col_name] 38 | if (!(column_total || column_total === 0)) { 39 | column_total = '' 40 | } 41 | footer_th += ``; 42 | } 43 | } 44 | let footer = ''; 45 | if (add_footer && stop_colspan_detection) { 46 | footer = '' + footer_th + ''; 47 | } 48 | return_val = return_val + header_th + `${footer}
${cols_names[i]}${column_total}
' + total_verbose + '
`; 49 | return return_val; 50 | } 51 | 52 | 53 | function buildAndInitializeDataTable(data, $elem, extraOptions, successFunction) { 54 | // Responsible for turning a ReportView Response into a datatable. 55 | 56 | let opts = $.extend({}, $.slick_reporting.datatable.defaults, extraOptions); 57 | opts['datatableContainer'] = $elem; 58 | 59 | let datatable_container = opts.datatableContainer; 60 | 61 | let provide_total = true; // typeof provide_total == 'undefined' ? true : provide_total; 62 | let total_fields = []; //# frontend_settings.total_fields || []; 63 | let column_names = []; 64 | for (let i = 0; i < data['columns'].length; i++) { 65 | let col = data['columns'][i]; 66 | column_names.push(col['verbose_name']); 67 | if (col['is_summable'] === true) { 68 | total_fields.push(col['name']) 69 | } 70 | } 71 | 72 | if (total_fields.length === 0) provide_total = false; 73 | 74 | datatable_container.html(constructTable( 75 | $.slick_reporting.datatable.defaults.tableCssClass, data['columns'], column_names, 76 | provide_total, opts.messages.total, total_fields, data.data)); 77 | initializeReportDatatable(datatable_container.find('table'), data, opts); 78 | 79 | if (typeof (successFunction) === 'function') { 80 | successFunction(data); 81 | } 82 | 83 | } 84 | 85 | 86 | function getDatatableColumns(data) { 87 | let columns = []; 88 | for (let i = 0; i < data['columns'].length; i++) { 89 | 90 | let server_data = data['columns'][i]; 91 | let col_data = { 92 | "data": server_data['name'], 93 | 'visible': server_data['visible'], 94 | 'title': server_data['verbose_name'] 95 | }; 96 | columns.push(col_data); 97 | 98 | } 99 | return columns; 100 | } 101 | 102 | 103 | function initializeReportDatatable(tableSelector, data, extraOptions) { 104 | tableSelector = typeof tableSelector != 'undefined' ? tableSelector : '.datatable'; 105 | extraOptions = typeof extraOptions != 'undefined' ? extraOptions : {}; 106 | 107 | let opts = $.extend({}, $.slick_reporting.datatable.defaults, extraOptions); 108 | 109 | 110 | let dom = typeof (extraOptions.dom) == 'undefined' ? 'lfrtip' : extraOptions.dom; 111 | let paging = typeof (extraOptions.paging) == 'undefined' ? true : extraOptions.paging; 112 | let ordering = typeof (extraOptions.ordering) == 'undefined' ? true : extraOptions.ordering; 113 | let info = typeof (extraOptions.info) == 'undefined' ? true : extraOptions.info; 114 | let searching = typeof (extraOptions.searching) == 'undefined' ? true : extraOptions.searching; 115 | if (data.data.length === 0) dom = '<"mb-20"t>'; 116 | 117 | let datatableOptions = $.extend({}, extraOptions['datatableOptions']); 118 | 119 | datatableOptions.dom = dom; 120 | datatableOptions.ordering = ordering; 121 | datatableOptions.paging = paging; 122 | datatableOptions.info = info; 123 | datatableOptions.searching = searching; 124 | 125 | datatableOptions.sorting = []; 126 | datatableOptions.processing = true; 127 | datatableOptions.data = data['data']; 128 | datatableOptions.columns = getDatatableColumns(data); 129 | datatableOptions.initComplete = function (settings, json) { 130 | setTimeout(function () { 131 | if (opts.enableFixedHeader) { 132 | new $.fn.dataTable.FixedHeader(dt, {"zTop": "2001"}); 133 | } 134 | }, 100); 135 | 136 | }; 137 | _instances[data.report_slug] = $(tableSelector).DataTable(datatableOptions); 138 | } 139 | 140 | 141 | $.slick_reporting.datatable = { 142 | initializeDataTable: initializeReportDatatable, 143 | _cache: _cache, 144 | buildAdnInitializeDatatable: buildAndInitializeDataTable, 145 | constructTable: constructTable, 146 | instances: _instances 147 | } 148 | }(jQuery)); 149 | 150 | $.slick_reporting.datatable.defaults = { 151 | 152 | enableFixedHeader: false, 153 | fixedHeaderZindex: 2001, 154 | messages: { 155 | total: $.slick_reporting.defaults.total_label, 156 | }, 157 | tableCssClass: 'table table-xxs datatable-basic table-bordered table-striped table-hover ', 158 | 159 | datatableOptions: { // datatables options sent to its constructor. 160 | css_class: 'display' 161 | 162 | } 163 | }; -------------------------------------------------------------------------------- /slick_reporting/static/slick_reporting/slick_reporting.report_loader.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | 3 | /** 4 | * Created by ramezashraf on 13/08/16. 5 | */ 6 | 7 | (function ($) { 8 | let settings = {}; 9 | 10 | function failFunction(data, $elem) { 11 | if (data.status === 403) { 12 | $elem.hide() 13 | } else { 14 | console.log(data, $elem) 15 | } 16 | } 17 | 18 | function loadComponents(data, $elem) { 19 | let chartElem = $elem.find('[data-report-chart]'); 20 | let chart_id = $elem.attr('data-chart-id'); 21 | let display_chart_selector = $elem.attr('data-display-chart-selector'); 22 | if (chartElem.length !== 0 && data.chart_settings.length !== 0) { 23 | 24 | $.slick_reporting.report_loader.displayChart(data, chartElem, chart_id); 25 | } 26 | 27 | if (display_chart_selector !== "False" && data.chart_settings.length > 1) { 28 | $.slick_reporting.report_loader.createChartsUIfromResponse(data, $elem); 29 | } 30 | 31 | let tableElem = $elem.find('[data-report-table]'); 32 | if (tableElem.length !== 0) { 33 | $.slick_reporting.datatable.buildAdnInitializeDatatable(data, tableElem); 34 | } 35 | 36 | } 37 | 38 | function displayChart(data, $elem, chart_id) { 39 | let engine = "highcharts"; 40 | let chartOptions = $.slick_reporting.getObjFromArray(data.chart_settings, 'id', chart_id, true); 41 | let entryPoint = chartOptions.entryPoint || $.slick_reporting.report_loader.chart_engines[engine]; 42 | $.slick_reporting.executeFunctionByName(entryPoint, window, data, $elem, chartOptions); 43 | } 44 | 45 | 46 | function refreshReportWidget($elem, extra_params) { 47 | let successFunctionName = $elem.attr('data-success-callback'); 48 | successFunctionName = successFunctionName || "$.slick_reporting.report_loader.successCallback"; 49 | let failFunctionName = $elem.attr('data-fail-callback'); 50 | failFunctionName = failFunctionName || "$.slick_reporting.report_loader.failFunction"; 51 | 52 | let data = {}; 53 | 54 | let url = $elem.attr('data-report-url'); 55 | extra_params = extra_params || '' 56 | let extraParams = extra_params + ($elem.attr('data-extra-params') || ''); 57 | 58 | let formSelector = $elem.attr('data-form-selector'); 59 | if (formSelector) { 60 | data = $(formSelector).serialize(); 61 | } else { 62 | if (url === '#') return; // there is no actual url, probably not enough permissions 63 | 64 | if (extraParams !== '') { 65 | url = url + "?" + extraParams; 66 | } 67 | 68 | } 69 | 70 | $.get(url, data, function (data) { 71 | $.slick_reporting.cache[data['report_slug']] = jQuery.extend(true, {}, data); 72 | $.slick_reporting.executeFunctionByName(successFunctionName, window, data, $elem); 73 | }).fail(function (data) { 74 | $.slick_reporting.executeFunctionByName(failFunctionName, window, data, $elem); 75 | }); 76 | 77 | } 78 | 79 | 80 | function initialize() { 81 | settings = JSON.parse(document.getElementById('slick_reporting_settings').textContent); 82 | let chartSettings = {}; 83 | $('[data-report-widget]').not('[data-no-auto-load]').each(function (i, elem) { 84 | refreshReportWidget($(elem)); 85 | }); 86 | 87 | Object.keys(settings["CHARTS"]).forEach(function (key) { 88 | chartSettings[key] = settings.CHARTS[key].entryPoint; 89 | }) 90 | $.slick_reporting.report_loader.chart_engines = chartSettings; 91 | try { 92 | $("select").select2(); 93 | } catch (e) { 94 | console.error(e); 95 | } 96 | $.slick_reporting.defaults.total_label = settings["MESSAGES"]["TOTAL_LABEL"]; 97 | } 98 | 99 | function _get_chart_icon(chart_type) { 100 | try { 101 | return ""; 102 | } catch (e) { 103 | console.error(e); 104 | } 105 | return ''; 106 | } 107 | 108 | function createChartsUIfromResponse(data, $elem, a_class) { 109 | a_class = typeof a_class == 'undefined' ? 'groupChartController' : a_class; 110 | let $container = $('
'); 111 | 112 | let chartList = data['chart_settings']; 113 | let report_slug = data['report_slug']; 114 | $elem.find('.groupChartControllers').remove(); 115 | if (chartList.length !== 0) { 116 | $container.append('
' + 117 | '
'); 118 | } 119 | var ul = $container.find('ul'); 120 | for (var i = 0; i < chartList.length; i++) { 121 | var icon; 122 | var chart = chartList[i]; 123 | if (chart.disabled) continue; 124 | 125 | let chart_type = chart.type; 126 | icon = _get_chart_icon(chart_type); 127 | 128 | ul.append('') 130 | } 131 | $elem.prepend($container) 132 | return $container 133 | } 134 | 135 | 136 | jQuery(document).ready(function () { 137 | $.slick_reporting.report_loader.initialize(); 138 | $('body').on('click', 'a[data-chart-id]', function (e) { 139 | e.preventDefault(); 140 | let $this = $(this); 141 | let data = $.slick_reporting.cache[$this.attr('data-report-slug')] 142 | let chart_id = $this.attr('data-chart-id') 143 | $.slick_reporting.report_loader.displayChart(data, $this.parents('[data-report-widget]').find('[data-report-chart]'), chart_id) 144 | 145 | }); 146 | 147 | $('[data-export-btn]').on('click', function (e) { 148 | let $elem = $(this); 149 | e.preventDefault() 150 | let form = $($elem.attr('data-form-selector')); 151 | window.location = '?' + form.serialize() + '&_export=' + $elem.attr('data-export-parameter'); 152 | }); 153 | $('[data-get-results-button]').not(".vanilla-btn-flag").on('click', function (event) { 154 | event.preventDefault(); 155 | let $elem = $('[data-report-widget]') 156 | $.slick_reporting.report_loader.refreshReportWidget($elem) 157 | }); 158 | 159 | }); 160 | 161 | 162 | $.slick_reporting.report_loader = { 163 | cache: $.slick_reporting.cache, 164 | // "extractDataFromResponse": extractDataFromResponse, 165 | initialize: initialize, 166 | refreshReportWidget: refreshReportWidget, 167 | failFunction: failFunction, 168 | displayChart: displayChart, 169 | createChartsUIfromResponse: createChartsUIfromResponse, 170 | successCallback: loadComponents, 171 | 172 | } 173 | })(jQuery); -------------------------------------------------------------------------------- /docs/source/topics/computation_field.rst: -------------------------------------------------------------------------------- 1 | .. _computation_field: 2 | 3 | 4 | Computation Field 5 | ================= 6 | 7 | ComputationFields are the basic unit in a report.they represent a number that is being computed. 8 | 9 | Computation Fields can be add to a report as a class, as you saw in other examples , or by name. 10 | 11 | 12 | Creating Computation Fields 13 | --------------------------- 14 | 15 | There are 3 ways you can create a Computation Field 16 | 17 | 1. Create a subclass of ComputationField and set the needed attributes and use it in the columns attribute of the ReportView 18 | 2. Use the `ComputationField.create()` method and pass the needed attributes and use it in the columns attribute of the ReportView 19 | 3. Use the `report_field_register` decorator to register a ComputationField subclass and use it by its name in the columns attribute of the ReportView 20 | 21 | 22 | 23 | .. code-block:: python 24 | 25 | from slick_reporting.fields import ComputationField 26 | from slick_reporting.decorators import report_field_register 27 | 28 | 29 | @report_field_register 30 | class TotalQTYReportField(ComputationField): 31 | name = "__total_quantity__" 32 | calculation_field = "quantity" # the field we want to compute on 33 | calculation_method = Sum # What method we want, default to Sum 34 | verbose_name = _("Total quantity") 35 | 36 | 37 | class ProductSales(ReportView): 38 | report_model = SalesTransaction 39 | # .. 40 | columns = [ 41 | # ... 42 | "__total_quantity__", # Use the ComputationField by its registered name 43 | TotalQTYReportField, # Use Computation Field as a class 44 | ComputationField.create( 45 | Sum, "quantity", name="__total_quantity__", verbose_name=_("Total quantity") 46 | ) 47 | # Using the ComputationField.create() method 48 | ] 49 | 50 | What happened here is that we: 51 | 52 | 1. Created a ComputationField subclass and gave it the needed attributes 53 | 2. Register it via ``report_field_register`` so it can be picked up by the framework. 54 | 3. Used it by name inside the columns attribute (or in time_series_columns, or in crosstab_columns) 55 | 4. Note that this is same as using the class directly in the columns , also the same as using `ComputationField.create()` 56 | 57 | Another example, adding and AVG to the field `price`: 58 | 59 | .. code-block:: python 60 | 61 | from django.db.models import Avg 62 | from slick_reporting.decorators import report_field_register 63 | 64 | 65 | @report_field_register 66 | class TotalQTYReportField(ComputationField): 67 | name = "__avg_price__" 68 | calculation_field = "price" 69 | calculation_method = Avg 70 | verbose_name = _("Avg. Price") 71 | 72 | 73 | class ProductSales(ReportView): 74 | # .. 75 | columns = [ 76 | "name", 77 | "__avg_price__", 78 | ] 79 | 80 | Using Value of a Computation Field within a another 81 | --------------------------------------------------- 82 | 83 | Sometime you want to stack values on top of each other. For example: Net revenue = Gross revenue - Discounts. 84 | 85 | .. code-block:: python 86 | 87 | class PercentageToTotalBalance(ComputationField): 88 | requires = [BalanceReportField] 89 | name = "__percent_to_total_balance__" 90 | verbose_name = _("%") 91 | calculation_method = Sum 92 | calculation_field = "value" 93 | 94 | prevent_group_by = True 95 | 96 | def resolve( 97 | self, 98 | prepared_results, 99 | required_computation_results: dict, 100 | current_pk, 101 | current_row=None, 102 | ) -> float: 103 | result = super().resolve( 104 | prepared_results, required_computation_results, current_pk, current_row 105 | ) 106 | return required_computation_results.get("__balance__") / result * 100 107 | 108 | 109 | We need to override ``resolve`` to do the needed calculation. The ``required_computation_results`` is a dictionary of the results of the required fields, where the keys are the names. 110 | 111 | Note: 112 | 113 | 1. The ``requires`` attribute is a list of the required fields to be computed before this field. 114 | 2. The values of the ``requires`` fields are available in the ``required_computation_results`` dictionary. 115 | 3. In the example we used the ``prevent_group_by`` attribute. It's as the name sounds, it prevents the rows from being grouped for teh ComputationField giving us the result over the whole set. 116 | 117 | 118 | How it works ? 119 | -------------- 120 | When the `ReportGenerator` is initialized, it generates a list of the needed fields to be displayed and computed. 121 | Each computation field in the report is given the filters needed and asked to get all the results prepared. 122 | Then for each record, the ReportGenerator again asks each ComputationField to get the data it has for each record and map it back. 123 | 124 | 125 | Customizing the Calculation Flow 126 | -------------------------------- 127 | 128 | The results are prepared in 2 main stages 129 | 130 | 1. Preparation: Where you can get the whole result set for the report. Example: Sum of all the values in a model group by the products. 131 | 2. resolve: Where you get the value for each record. 132 | 133 | 134 | 135 | 136 | .. code-block:: python 137 | 138 | class MyCustomComputationField(ComputationField): 139 | name = "__custom_field__" 140 | 141 | def prepare( 142 | self, 143 | q_filters: list | object = None, 144 | kwargs_filters: dict = None, 145 | queryset=None, 146 | **kwargs 147 | ): 148 | # do all you calculation here for the whole set if any and return the prepared results 149 | pass 150 | 151 | def resolve( 152 | self, 153 | prepared_results, 154 | required_computation_results: dict, 155 | current_pk, 156 | current_row=None, 157 | ) -> float: 158 | # does the calculation for each record, return a value 159 | pass 160 | 161 | Bundled Report Fields 162 | --------------------- 163 | As this project came form an ERP background, there are some bundled report fields that you can use out of the box. 164 | 165 | * __total__ : `Sum` of the field named `value` 166 | * __total_quantity__ : `Sum` of the field named `quantity` 167 | * __fb__ : First Balance, Sum of the field `value` on the start date (or period in case of time series) 168 | * __balance__: Compound Sum of the field `value`. IE: the sum of the field `value` on end date. 169 | * __credit__: Sum of field Value for the minus_list 170 | * __debit__: Sum of the field value for the plus list 171 | * __percent_to_total_balance__: Percent of the field value to the balance 172 | 173 | What is the difference between total and balance fields ? 174 | 175 | Total: Sum of the value for the period 176 | Balance: Sum of the value for the period + all the previous periods. 177 | 178 | Example: You have a client who buys 10 in Jan., 12 in Feb. and 13 in March: 179 | 180 | * `__total__` will return 10 in Jan, 12 in Feb and 13 in March. 181 | * `__balance__` will return 10 in Jan, 22 in Feb and 35 in March 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /docs/source/tour.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | A walk through 4 | ============== 5 | 6 | Update 7 | ~~~~~~ 8 | You can now go to https://django-slick-reporting for a better and practical guidance on the types of reports and what you can do. 9 | 10 | 11 | 12 | Given that you have a model where there are data stored which you want to generate reports on. 13 | Consider below SalesOrder model example. 14 | 15 | +------------+------------+-----------+----------+-------+-------+ 16 | | order_date | product_id | client_id | quantity | price | value | 17 | +------------+------------+-----------+----------+-------+-------+ 18 | | 2019-01-01 | 1 | 1 | 5 | 15 | 75 | 19 | +------------+------------+-----------+----------+-------+-------+ 20 | | 2019-02-15 | 2 | 2 | 7 | 20 | 140 | 21 | +------------+------------+-----------+----------+-------+-------+ 22 | | 2019-02-20 | 2 | 1 | 5 | 20 | 100 | 23 | +------------+------------+-----------+----------+-------+-------+ 24 | | 2019-03-14 | 1 | 2 | 3 | 15 | 45 | 25 | +------------+------------+-----------+----------+-------+-------+ 26 | 27 | Slick Reporting help us answer some questions, like: 28 | 29 | * To start: Wouldn't it be nice if we have a view page where we can filter the data based on date , client(s) and or product(s) 30 | * How much each product was sold or How much each Client bought? Filter by date range / client(s) / product(s) 31 | * How well each product sales is doing, monthly? 32 | * How client 1 compared with client 2, compared with the rest of clients, on each product sales ? 33 | * How many orders were created a day ? 34 | 35 | To answer those question, We can identify basic kind of alteration / calculation on the data 36 | 37 | 38 | 1. Basic filtering 39 | ------------------ 40 | 41 | Start small, 42 | A ReportView like the below 43 | 44 | .. code-block:: python 45 | 46 | # in your urls.py 47 | path("path-to-report", TransactionsReport.as_view()) 48 | 49 | # in your views.py 50 | from slick_reporting.views import ReportView 51 | 52 | 53 | class TransactionsReport(ReportView): 54 | report_model = MySalesItem 55 | columns = [ 56 | "order_date", 57 | "product__name", 58 | "client__name", 59 | "quantity", 60 | "price", 61 | "value", 62 | ] 63 | 64 | 65 | will yield a Page with a nice filter form with 66 | A report where it displays the data as is but with filters however we can apply date and other filters 67 | 68 | +------------+---------------+-------------+----------+-------+-------+ 69 | | order_date | Product Name | Client Name | quantity | price | value | 70 | +------------+---------------+-------------+----------+-------+-------+ 71 | | 2019-01-01 | Product 1 | Client 1 | 5 | 15 | 75 | 72 | +------------+---------------+-------------+----------+-------+-------+ 73 | | 2019-02-15 | Product 2 | Client 2 | 7 | 20 | 140 | 74 | +------------+---------------+-------------+----------+-------+-------+ 75 | | 2019-02-20 | Product 2 | Client 1 | 5 | 20 | 100 | 76 | +------------+---------------+-------------+----------+-------+-------+ 77 | | 2019-03-14 | Product 1 | Client 2 | 3 | 15 | 45 | 78 | +------------+---------------+-------------+----------+-------+-------+ 79 | 80 | 2. Group By report 81 | ------------------- 82 | 83 | Where we can group by product -for example- and sum the quantity, or value sold. 84 | 85 | +-----------+----------------+-------------+ 86 | | Product | Total Quantity | Total Value | 87 | +-----------+----------------+-------------+ 88 | | Product 1 | 8 | 120 | 89 | +-----------+----------------+-------------+ 90 | | Product 2 | 13 | 240 | 91 | +-----------+----------------+-------------+ 92 | 93 | which can be written like this: 94 | 95 | .. code-block:: python 96 | 97 | class TotalQuanAndValueReport(ReportView): 98 | report_model = MySalesItem 99 | group_by = "product" 100 | columns = ["name", "__total_quantity__", "__total__"] 101 | 102 | 103 | 104 | 3. Time Series report 105 | ------------------------ 106 | 107 | where we can say how much sum of the quantity sold over a chunks of time periods (like weekly, monthly, ... ) 108 | 109 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 110 | | Product Name | SKU | Total Quantity | Total Quantity | Total Quantity in ... | Total Quantity in December 20 | 111 | | | | in Jan 20 | in Feb 20 | | | 112 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 113 | | Product 1 | | 5 | 0 | ... | 14 | 114 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 115 | | Product 2 | | 0 | 13 | ... | 12 | 116 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 117 | | Product 3 | | 17 | 12 | ... | 17 | 118 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 119 | 120 | can be written like this 121 | 122 | .. code-block:: python 123 | 124 | class TotalQuantityMonthly(ReportView): 125 | report_model = MySalesItem 126 | group_by = "product" 127 | columns = ["name", "sku"] 128 | 129 | time_series_pattern = "monthly" 130 | time_series_columns = ["__total_quantity__"] 131 | 132 | 133 | 4. Cross tab report 134 | -------------------- 135 | 136 | Where we can cross product sold over client for example 137 | 138 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 139 | | Product Name | SKU | Client 1 | Client 2 | Client (n) | The Reminder | 140 | | | | Total value | Total Value | | | 141 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 142 | | Product 1 | | 10 | 15 | ... | 14 | 143 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 144 | | Product 2 | | 11 | 12 | ... | 12 | 145 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 146 | | Product 3 | | 17 | 12 | ... | 17 | 147 | +--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+ 148 | 149 | Which can be written like this 150 | 151 | .. code-block:: python 152 | 153 | class CrosstabProductClientValue(ReportView): 154 | report_model = MySalesItem 155 | group_by = "product" 156 | columns = ["name", "sku"] 157 | 158 | crosstab_model = "client" 159 | crosstab_columns = ["__total_value__"] 160 | crosstab_ids = [client1.pk, client2.pk, client3.pk] 161 | crosstab_compute_remainder = True 162 | 163 | 164 | 165 | 5. Time series - Cross tab 166 | -------------------------- 167 | (#2 & #3 together) Not support at the time.. but soon we hope. 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /slick_reporting/static/slick_reporting/slick_reporting.chartsjs.js: -------------------------------------------------------------------------------- 1 | // type / title_source / data_source, 2 | // title 3 | 4 | (function ($) { 5 | 6 | 7 | var COLORS = ['#7cb5ec', '#f7a35c', '#90ee7e', '#7798BF', '#aaeeee', '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee']; 8 | 9 | let _chart_cache = {}; 10 | 11 | function is_time_series(response, chartOptions) { 12 | if (chartOptions.time_series_support === false) return false; 13 | return response['metadata']['time_series_pattern'] == "" 14 | } 15 | 16 | function getTimeSeriesColumnNames(response) { 17 | return response['metadata']['time_series_column_names']; 18 | } 19 | 20 | function createChartObject(response, chartOptions, extraOptions) { 21 | // let chartOptions = $.slick_reporting.getObjFromArray(response.chart_settings, 'id', chartId, true); 22 | let extractedData = extractDataFromResponse(response, chartOptions); 23 | 24 | let chartObject = { 25 | type: chartOptions.type, 26 | 'data': { 27 | labels: extractedData.labels, 28 | datasets: extractedData.datasets, 29 | 30 | // datasets: [{ 31 | // 'label': chartOptions.title, 32 | // // 'label': extractedData.labels, 33 | // 'data': extractedData.data, 34 | // 'borderWidth': 1, 35 | // 36 | // backgroundColor: [ 37 | // window.chartColors.red, 38 | // window.chartColors.orange, 39 | // window.chartColors.yellow, 40 | // window.chartColors.green, 41 | // window.chartColors.blue, 42 | // ], 43 | // }] 44 | }, 45 | 'options': { 46 | 'responsive': true, 47 | title: { 48 | display: true, 49 | text: chartOptions.title, 50 | }, 51 | tooltips: { 52 | mode: 'index', 53 | // intersect: false 54 | }, 55 | } 56 | }; 57 | 58 | if (chartOptions.type === 'pie') { 59 | chartObject['options'] = { 60 | responsive: true, 61 | } 62 | } 63 | if (chartOptions.stacked === true) { 64 | chartObject['options']['scales'] = { 65 | yAxes: [{stacked: true,}], 66 | XAxes: [{stacked: true,}], 67 | } 68 | } 69 | return chartObject 70 | } 71 | 72 | function getGroupByLabelAndSeries(response, chartOptions) { 73 | 74 | let legendResults = []; 75 | let datasetData = []; 76 | let dataFieldName = chartOptions['data_source']; 77 | let titleFieldName = chartOptions['title_source']; 78 | 79 | for (let i = 0; i < response.data.length; i++) { 80 | let row = response.data[i]; 81 | if (titleFieldName !== '') { 82 | let txt = row[titleFieldName]; 83 | txt = $(txt).text() || txt; // the title is an "); 166 | } 167 | 168 | let cache_key = data.report_slug 169 | try { 170 | let existing_chart = _chart_cache[cache_key]; 171 | if (typeof (existing_chart) !== 'undefined') { 172 | existing_chart.destroy(); 173 | } 174 | } catch (e) { 175 | console.error(e) 176 | } 177 | 178 | let chartObject = $.slick_reporting.chartsjs.createChartObject(data, chartOptions); 179 | let $chart = $elem.find('canvas'); 180 | try { 181 | _chart_cache[cache_key] = new Chart($chart, chartObject); 182 | } catch (e) { 183 | $elem.find('canvas').remove(); 184 | } 185 | 186 | 187 | } 188 | 189 | 190 | if (typeof ($.slick_reporting) === 'undefined') { 191 | $.slick_reporting = {} 192 | } 193 | $.slick_reporting.chartsjs = { 194 | getGroupByLabelAndSeries: getGroupByLabelAndSeries, 195 | createChartObject: createChartObject, 196 | displayChart: displayChart, 197 | defaults: { 198 | // normalStackedTooltipFormatter: normalStackedTooltipFormatter, 199 | messages: { 200 | noData: 'No Data to display ... :-/', 201 | total: 'Total', 202 | percent: 'Percent', 203 | }, 204 | credits: { 205 | // text: 'RaSystems.io', 206 | // href: 'https://rasystems.io' 207 | }, 208 | notify_error: function () { 209 | }, 210 | enable3d: false, 211 | 212 | } 213 | }; 214 | 215 | }(jQuery)); -------------------------------------------------------------------------------- /docs/source/topics/time_series_options.rst: -------------------------------------------------------------------------------- 1 | .. _time_series: 2 | 3 | Time Series Reports 4 | =================== 5 | 6 | A Time series report is a report that is generated for a periods of time. 7 | The period can be daily, weekly, monthly, yearly or custom, calculations will be performed for each period in the time series. 8 | 9 | General use case 10 | ---------------- 11 | 12 | Here is a quick look at the general use case 13 | 14 | 15 | .. code-block:: python 16 | 17 | from django.utils.translation import gettext_lazy as _ 18 | from django.db.models import Sum 19 | from slick_reporting.views import ReportView 20 | 21 | 22 | class TimeSeriesReport(ReportView): 23 | report_model = SalesTransaction 24 | group_by = "client" 25 | 26 | time_series_pattern = "monthly" 27 | # options are: "daily", "weekly", "bi-weekly", "monthly", "quarterly", "semiannually", "annually" and "custom" 28 | 29 | date_field = "date" 30 | 31 | # These columns will be calculated for each period in the time series. 32 | time_series_columns = [ 33 | ComputationField.create(Sum, "value", verbose_name=_("Sales For Month")), 34 | ] 35 | 36 | columns = [ 37 | "name", 38 | "__time_series__", 39 | # This is the same as the time_series_columns, but this one will be on the whole set 40 | ComputationField.create(Sum, "value", verbose_name=_("Total Sales")), 41 | ] 42 | 43 | chart_settings = [ 44 | Chart( 45 | "Client Sales", 46 | Chart.BAR, 47 | data_source=["sum__value"], 48 | title_source=["name"], 49 | ), 50 | Chart( 51 | "Total Sales Monthly", 52 | Chart.PIE, 53 | data_source=["sum__value"], 54 | title_source=["name"], 55 | plot_total=True, 56 | ), 57 | Chart( 58 | "Total Sales [Area chart]", 59 | Chart.AREA, 60 | data_source=["sum__value"], 61 | title_source=["name"], 62 | ), 63 | ] 64 | 65 | 66 | Allowing the User to Choose the time series pattern 67 | --------------------------------------------------- 68 | 69 | You can allow the User to Set the Pattern for the report , Let's create another version of the above report 70 | where the user can choose the pattern 71 | 72 | .. code-block:: python 73 | 74 | class TimeSeriesReportWithSelector(TimeSeriesReport): 75 | report_title = _("Time Series Report With Pattern Selector") 76 | time_series_selector = True 77 | time_series_selector_choices = ( 78 | ("daily", _("Daily")), 79 | ("weekly", _("Weekly")), 80 | ("bi-weekly", _("Bi-Weekly")), 81 | ("monthly", _("Monthly")), 82 | ) 83 | time_series_selector_default = "bi-weekly" 84 | 85 | time_series_selector_label = _("Period Pattern") 86 | # The label for the time series selector 87 | 88 | time_series_selector_allow_empty = True 89 | # Allow the user to select an empty time series, in which case no time series will be applied to the report. 90 | 91 | 92 | Set Custom Dates for the Time Series 93 | ------------------------------------ 94 | 95 | You might want to set irregular pattern for the time series, 96 | Like first 10 days of each month , or the 3 summer month of every year. 97 | 98 | Let's see how you can do that, inheriting from teh same Time series we did first. 99 | 100 | .. code-block:: python 101 | 102 | 103 | def get_current_year(): 104 | return datetime.datetime.now().year 105 | 106 | 107 | class TimeSeriesReportWithCustomDates(TimeSeriesReport): 108 | report_title = _("Time Series Report With Custom Dates") 109 | time_series_pattern = "custom" 110 | time_series_custom_dates = ( 111 | ( 112 | datetime.datetime(get_current_year(), 1, 1), 113 | datetime.datetime(get_current_year(), 1, 10), 114 | ), 115 | ( 116 | datetime.datetime(get_current_year(), 2, 1), 117 | datetime.datetime(get_current_year(), 2, 10), 118 | ), 119 | ( 120 | datetime.datetime(get_current_year(), 3, 1), 121 | datetime.datetime(get_current_year(), 3, 10), 122 | ), 123 | ) 124 | 125 | 126 | 127 | Customize the Computation Field label 128 | ------------------------------------- 129 | Maybe you want to customize how the title of the time series computation field. 130 | For this you want to Subclass ``ComputationField``, where you can customize 131 | how the title is created and use it in the time_series_column instead of the one created on the fly. 132 | 133 | Example: 134 | 135 | .. code-block:: python 136 | 137 | 138 | class SumOfFieldValue(ComputationField): 139 | # A custom computation Field identical to the one created like this 140 | # Similar to `ComputationField.create(Sum, "value", verbose_name=_("Total Sales"))` 141 | 142 | calculation_method = Sum 143 | calculation_field = "value" 144 | name = "sum_of_value" 145 | 146 | @classmethod 147 | def get_time_series_field_verbose_name(cls, date_period, index, dates, pattern): 148 | # date_period: is a tuple (start_date, end_date) 149 | # index is the index of the current pattern in the patterns on the report 150 | # dates: the whole dates we have on the reports 151 | # pattern it's the pattern name, ex: monthly, daily, custom 152 | return f"First 10 days sales {date_period[0].month}-{date_period[0].year}" 153 | 154 | 155 | class TimeSeriesReportWithCustomDatesAndCustomTitle(TimeSeriesReportWithCustomDates): 156 | report_title = _("Time Series Report With Custom Dates and custom Title") 157 | 158 | time_series_columns = [ 159 | SumOfFieldValue, # Use our newly created ComputationField with the custom time series verbose name 160 | ] 161 | 162 | chart_settings = [ 163 | Chart( 164 | "Client Sales", 165 | Chart.BAR, 166 | data_source=[ 167 | "sum_of_value" 168 | ], # Note: This is the name of our `TotalSalesField` `field 169 | title_source=["name"], 170 | ), 171 | Chart( 172 | "Total Sales [Pie]", 173 | Chart.PIE, 174 | data_source=["sum_of_value"], 175 | title_source=["name"], 176 | plot_total=True, 177 | ), 178 | ] 179 | 180 | 181 | Time Series without a group by 182 | ------------------------------ 183 | Maybe you want to get the time series calculated on the whole set, without grouping by anything. 184 | You can do that by omitting the `group_by` attribute, and having only time series (or other computation fields) columns. 185 | 186 | Example: 187 | 188 | .. code-block:: python 189 | 190 | class TimeSeriesWithoutGroupBy(ReportView): 191 | report_title = _("Time Series without a group by") 192 | report_model = SalesTransaction 193 | time_series_pattern = "monthly" 194 | date_field = "date" 195 | time_series_columns = [ 196 | ComputationField.create(Sum, "value", verbose_name=_("Sales For ")), 197 | ] 198 | 199 | columns = [ 200 | "__time_series__", 201 | ComputationField.create(Sum, "value", verbose_name=_("Total Sales")), 202 | ] 203 | 204 | chart_settings = [ 205 | Chart( 206 | "Total Sales [Bar]", 207 | Chart.BAR, 208 | data_source=["sum__value"], 209 | title_source=["name"], 210 | ), 211 | Chart( 212 | "Total Sales [Pie]", 213 | Chart.PIE, 214 | data_source=["sum__value"], 215 | title_source=["name"], 216 | ), 217 | ] 218 | 219 | 220 | 221 | 222 | .. _time_series_options: 223 | 224 | Time Series Options 225 | ------------------- 226 | 227 | .. attribute:: ReportView.time_series_pattern 228 | 229 | the time series pattern to be used in the report, it can be one of the following: 230 | Possible options are: daily, weekly, semimonthly, monthly, quarterly, semiannually, annually and custom. 231 | if `custom` is set, you'd need to override `time_series_custom_dates` 232 | 233 | .. attribute:: ReportView.time_series_custom_dates 234 | 235 | A list of tuples of (start_date, end_date) pairs indicating the start and end of each period. 236 | 237 | .. attribute:: ReportView.time_series_columns 238 | 239 | a list of Calculation Field names which will be included in the series calculation. 240 | 241 | .. code-block:: python 242 | 243 | class MyReport(ReportView): 244 | 245 | time_series_columns = [ 246 | ComputationField.create( 247 | Sum, "value", verbose_name=_("Value"), is_summable=True, name="sum__value" 248 | ), 249 | ComputationField.create( 250 | Avg, "Price", verbose_name=_("Avg Price"), is_summable=False 251 | ), 252 | ] 253 | 254 | 255 | 256 | 257 | 258 | Links to demo 259 | '''''''''''''' 260 | 261 | Time series Selector pattern `Demo `_ 262 | and the `Code on github `_ for it. 263 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/django-slick-reporting.svg 2 | :target: https://pypi.org/project/django-slick-reporting 3 | 4 | .. image:: https://img.shields.io/pypi/pyversions/django-slick-reporting.svg 5 | :target: https://pypi.org/project/django-slick-reporting 6 | 7 | .. image:: https://img.shields.io/readthedocs/django-slick-reporting 8 | :target: https://django-slick-reporting.readthedocs.io/ 9 | 10 | .. image:: https://api.travis-ci.com/ra-systems/django-slick-reporting.svg?branch=master 11 | :target: https://app.travis-ci.com/github/ra-systems/django-slick-reporting 12 | 13 | .. image:: https://img.shields.io/codecov/c/github/ra-systems/django-slick-reporting 14 | :target: https://codecov.io/gh/ra-systems/django-slick-reporting 15 | 16 | 17 | 18 | 19 | Django Slick Reporting 20 | ====================== 21 | 22 | A one stop reports engine with batteries included. 23 | 24 | Features 25 | -------- 26 | 27 | - Effortlessly create Simple, Grouped, Time series and Crosstab reports in a handful of code lines. 28 | - Create Chart(s) for your reports with a single line of code. 29 | - Create Custom complex Calculation. 30 | - Optimized for speed. 31 | - Easily extendable. 32 | 33 | Installation 34 | ------------ 35 | 36 | Use the package manager `pip `_ to install django-slick-reporting. 37 | 38 | .. code-block:: console 39 | 40 | pip install django-slick-reporting 41 | 42 | 43 | Usage 44 | ----- 45 | 46 | So we have a model `SalesTransaction` which contains typical data about a sale. 47 | We can extract different kinds of information for that model. 48 | 49 | Let's start by a "Group by" report. This will generate a report how much quantity and value was each product sold within a certain time. 50 | 51 | .. code-block:: python 52 | 53 | 54 | # in views.py 55 | from django.db.models import Sum 56 | from slick_reporting.views import ReportView, Chart 57 | from slick_reporting.fields import ComputationField 58 | from .models import MySalesItems 59 | 60 | 61 | class TotalProductSales(ReportView): 62 | report_model = SalesTransaction 63 | date_field = "date" 64 | group_by = "product" 65 | columns = [ 66 | "name", 67 | ComputationField.create( 68 | Sum, "quantity", verbose_name="Total quantity sold", is_summable=False 69 | ), 70 | ComputationField.create( 71 | Sum, "value", name="sum__value", verbose_name="Total Value sold $" 72 | ), 73 | ] 74 | 75 | chart_settings = [ 76 | Chart( 77 | "Total sold $", 78 | Chart.BAR, 79 | data_source=["sum__value"], 80 | title_source=["name"], 81 | ), 82 | Chart( 83 | "Total sold $ [PIE]", 84 | Chart.PIE, 85 | data_source=["sum__value"], 86 | title_source=["name"], 87 | ), 88 | ] 89 | 90 | 91 | # then, in urls.py 92 | path("total-sales-report", TotalProductSales.as_view()) 93 | 94 | 95 | 96 | With this code, you will get something like this: 97 | 98 | .. image:: https://i.ibb.co/SvxTM23/Selection-294.png 99 | :target: https://i.ibb.co/SvxTM23/Selection-294.png 100 | :alt: Shipped in View Page 101 | 102 | 103 | Time Series 104 | ----------- 105 | 106 | A Time series report is a report that is generated for a periods of time. 107 | The period can be daily, weekly, monthly, yearly or custom. Calculations will be performed for each period in the time series. 108 | 109 | Example: How much was sold in value for each product monthly within a date period ? 110 | 111 | .. code-block:: python 112 | 113 | # in views.py 114 | from slick_reporting.views import ReportView 115 | from slick_reporting.fields import ComputationField 116 | from .models import SalesTransaction 117 | 118 | 119 | class MonthlyProductSales(ReportView): 120 | report_model = SalesTransaction 121 | date_field = "date" 122 | group_by = "product" 123 | columns = ["name", "sku"] 124 | 125 | time_series_pattern = "monthly" 126 | # or "yearly" , "weekly" , "daily" , others and custom patterns 127 | time_series_columns = [ 128 | ComputationField.create( 129 | Sum, "value", verbose_name=_("Sales Value"), name="value" 130 | ) # what will be calculated for each month 131 | ] 132 | 133 | chart_settings = [ 134 | Chart( 135 | _("Total Sales Monthly"), 136 | Chart.PIE, 137 | data_source=["value"], 138 | title_source=["name"], 139 | plot_total=True, 140 | ), 141 | Chart( 142 | "Total Sales [Area chart]", 143 | Chart.AREA, 144 | data_source=["value"], 145 | title_source=["name"], 146 | plot_total=False, 147 | ), 148 | ] 149 | 150 | 151 | .. image:: https://github.com/ra-systems/django-slick-reporting/blob/develop/docs/source/topics/_static/timeseries.png?raw=true 152 | :alt: Time Series Report 153 | :align: center 154 | 155 | Cross Tab 156 | --------- 157 | Use crosstab reports, also known as matrix reports, to show the relationships between three or more query items. 158 | Crosstab reports show data in rows and columns with information summarized at the intersection points. 159 | 160 | .. code-block:: python 161 | 162 | # in views.py 163 | from slick_reporting.views import ReportView 164 | from slick_reporting.fields import ComputationField 165 | from .models import MySalesItems 166 | 167 | 168 | class MyCrosstabReport(ReportView): 169 | 170 | crosstab_field = "client" 171 | crosstab_ids = [1, 2, 3] 172 | crosstab_columns = [ 173 | ComputationField.create(Sum, "value", verbose_name=_("Value for")), 174 | ] 175 | crosstab_compute_remainder = True 176 | 177 | columns = [ 178 | "some_optional_field", 179 | # You can customize where the crosstab columns are displayed in relation to the other columns 180 | "__crosstab__", 181 | # This is the same as the Same as the calculation in the crosstab, but this one will be on the whole set. IE total value 182 | ComputationField.create(Sum, "value", verbose_name=_("Total Value")), 183 | ] 184 | 185 | 186 | .. image:: https://github.com/ra-systems/django-slick-reporting/blob/develop/docs/source/topics/_static/crosstab.png?raw=true 187 | :alt: Homepage 188 | :align: center 189 | 190 | 191 | Low level 192 | --------- 193 | 194 | The view is a wrapper over the `ReportGenerator` class, which is the core of the reporting engine. 195 | You can interact with the `ReportGenerator` using same syntax as used with the `ReportView` . 196 | 197 | .. code-block:: python 198 | 199 | from slick_reporting.generator import ReportGenerator 200 | from .models import MySalesModel 201 | 202 | 203 | class MyReport(ReportGenerator): 204 | report_model = MySalesModel 205 | group_by = "product" 206 | columns = ["title", "__total__"] 207 | 208 | 209 | # OR 210 | my_report = ReportGenerator( 211 | report_model=MySalesModel, group_by="product", columns=["title", "__total__"] 212 | ) 213 | my_report.get_report_data() # -> [{'title':'Product 1', '__total__: 56}, {'title':'Product 2', '__total__: 43}, ] 214 | 215 | 216 | This is just a scratch of what you can do and customize. 217 | 218 | Demo site 219 | --------- 220 | 221 | Available on `Django Slick Reporting `_ 222 | 223 | 224 | You can also use locally 225 | 226 | .. code-block:: console 227 | 228 | # clone the repo 229 | git clone https://github.com/ra-systems/django-slick-reporting.git 230 | # create a virtual environment and activate it 231 | python -m venv /path/to/new/virtual/environment 232 | source /path/to/new/virtual/environment/bin/activate 233 | 234 | cd django-slick-reporting/demo_proj 235 | pip install -r requirements.txt 236 | python manage.py migrate 237 | python manage.py create_entries 238 | python manage.py runserver 239 | 240 | the ``create_entries`` command will generate data for the demo app 241 | 242 | Documentation 243 | ------------- 244 | 245 | Available on `Read The Docs `_ 246 | 247 | You can run documentation locally 248 | 249 | .. code-block:: console 250 | 251 | 252 | cd docs 253 | pip install -r requirements.txt 254 | sphinx-build -b html source build 255 | 256 | 257 | Road Ahead 258 | ---------- 259 | 260 | * Continue on enriching the demo project 261 | * Add the dashboard capabilities 262 | 263 | 264 | Running tests 265 | ----------------- 266 | Create a virtual environment (maybe with `virtual slick_reports_test`), activate it; Then , 267 | 268 | .. code-block:: console 269 | 270 | $ git clone git+git@github.com:ra-systems/django-slick-reporting.git 271 | $ cd tests 272 | $ python -m pip install -e .. 273 | 274 | $ python runtests.py 275 | # Or for Coverage report 276 | $ coverage run --include=../* runtests.py [-k] 277 | $ coverage html 278 | 279 | 280 | Support & Contributing 281 | ---------------------- 282 | 283 | Please consider star the project to keep an eye on it. Your PRs, reviews are most welcome and needed. 284 | 285 | We honor the well formulated `Django's guidelines `_ to serve as contribution guide here too. 286 | 287 | 288 | Authors 289 | -------- 290 | 291 | * **Ramez Ashraf** - *Initial work* - `RamezIssac `_ 292 | 293 | Cross Reference 294 | --------------- 295 | 296 | If you like this package, chances are you may like those packages too! 297 | 298 | `Django Tabular Permissions `_ Display Django permissions in a HTML table that is translatable and easy customized. 299 | 300 | `Django ERP Framework `_ A framework to build business solutions with ease. 301 | 302 | If you find this project useful or promising , You can support us by a github ⭐ 303 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.3.1] - 2024-06-16 6 | - Fix issue with Line Chart on highcharts engine 7 | - Reintroduce the stacking option on highcharts engine. 8 | - Fix issue with having different version of the same chart on the same page. 9 | - Enhanced the demo dashboard to show more capabilities regarding the charts. 10 | 11 | ## [1.3.0] - 2023-11-08 12 | - Implement Slick reporting media override feature + docs 13 | - Add `Integrating reports into your Admin site` section to the docs 14 | - Group by and crosstab reports do not need date_field set anymore. Only time series do. 15 | - Fix in FirstBalance Computation field if no date is supplied 16 | - Add `REPORT_VIEW_ACCESS_FUNCTION` to control access to the report view 17 | 18 | 19 | ## [1.2.0] - 2023-10-10 20 | - Add ``get_slick_reporting_media`` and ``get_charts_media`` templatetags 21 | - Add `get_group_by_custom_querysets` hook to ReportView 22 | - Enhance and document adding export options and customizing the builtin export to csv button 23 | - Enhance and document adding custom buttons to the report page 24 | - Enhance and document adding a new chart engine 25 | - Fix in SlickReportingListView 26 | - Move all css and js resources to be handled by `Media` governed by `settings.SLICK_REPORTING_SETTINGS` 27 | 28 | 29 | ## [1.1.1] - 2023-09-25 30 | - Change settings to be a dict , adding support JQUERY_URL and FONT AWESOME customization #79 & #81 31 | - Fix issue with chartjs not being loaded #80 32 | - Remove `SLICK_REPORTING_FORM_MEDIA` 33 | 34 | ## [1.1.0] - 35 | - Breaking: changed ``report_title_context_key`` default value to `report_title` 36 | - Breaking: Renamed simple_report.html to report.html 37 | - Breaking: Renamed ``SlickReportField`` to ``ComputationField``. SlickReportField will continue to work till next release. 38 | - Revised and renamed js files 39 | - Add dashboard capabilities. 40 | - Added auto_load option to ReportView 41 | - Unified report loading to use the report loader 42 | - Fix issue with group_by_custom_queryset with time series 43 | - Fix issue with No group by report 44 | - Fix issue with traversing fields not showing up on ListViewReport 45 | - Fix issue with date filter not being respected in ListViewReport 46 | 47 | ## [1.0.2] - 2023-08-31 48 | - Add a demo project for exploration and also containing all documentation code for proofing. 49 | - Revise and Enhancing Tutorial , Group by and Time series documentation. 50 | - Fix issue with error on dev console on report page due to resources duplication 51 | - Fix issue with Custom querysets not being correctly connected in the view 52 | - Fix issue with time series custom dates 53 | - Fix issue with Crosstab on traversing fields 54 | 55 | 56 | ## [1.0.1] - 2023-07-03 57 | 58 | - Added missing js files ported from erp_framework package. 59 | - Document the need for "crispy_bootstrap4" in the docs and add it as a dependency in the setup. 60 | 61 | ## [1.0.0] - 2023-07-03 62 | 63 | - Added crosstab_ids_custom_filters to allow custom filters on crosstab ids 64 | - Added ``group_by_custom_querysets`` to allow custom querysets as group 65 | - Added ability to have crosstab report in a time series report 66 | - Enhanced Docs content and structure. 67 | 68 | ## [0.9.0] - 2023-06-07 69 | 70 | - Deprecated ``form_factory`` in favor of ``forms``, to be removed next version. 71 | - Deprecated `crosstab_model` in favor of ``crosstab_field``, to be removed next version. 72 | - Deprecated ``slick_reporting.view.SlickReportView`` and ``slick_reporting.view.SlickReportViewBase`` in favor of ``slick_reporting.view.ReportView`` and ``slick_reporting.view.BaseReportView``, to be removed next version. 73 | - Allowed cross tab on fields other than ForeignKey 74 | - Added support for start_date_field_name and end_date_field_name 75 | - Added support to crosstab on traversing fields 76 | - Added support for document types / debit and credit calculations 77 | - Added support for ordering via ``ReportView.default_order_by`` and/or passing the parameter ``order_by`` to the view 78 | - Added return of Ajax response in case of error and request is Ajax 79 | - Made it easy override to the search form. Create you own form and subclass BaseReportForm and implement the mandatory method(s). 80 | - Consolidated the needed resources in ``slick_reporting/js_resource.html`` template, so to use your own template you just need to include it. 81 | - Fixed an issue with report fields not respecting the queryset on the ReportView. 82 | - Fixed an issue if a foreign key have a custom `to_field` set either in ``group_by`` and/or `crosstab_field` . 83 | - Enhancing and adding to the documentation. 84 | - Black format the code and the documentation 85 | 86 | 87 | ## [0.8.0] 88 | 89 | - Breaking: [Only if you use Crosstab reports] renamed crosstab_compute_reminder to crosstab_compute_remainder 90 | - Breaking : [Only if you set the templates statics by hand] renamed slick_reporting to ra.hightchart.js and ra.chartjs.js to 91 | erp_framework.highchart.js and erp_framework.chartjs.js respectively 92 | - Fix an issue with Crosstab when there crosstab_compute_remainder = False 93 | 94 | ## [0.7.0] 95 | 96 | - Added SlickReportingListView: a Report Class to display content of the model (like a ModelAdmin ChangeList) 97 | - Added `show_time_series_selector` capability to SlickReportView allowing User to change the time series pattern from 98 | the UI. 99 | - Added ability to export to CSV from UI, using `ExportToStreamingCSV` & `ExportToCSV` 100 | - Now you can have a custom column defined on the SlickReportView (and not needing to customise the report generator). 101 | - You don't need to set date_field if you don't have calculations on the report 102 | - Easier customization of the crispy form layout 103 | - Enhance weekly time series default column name 104 | - Add `Chart` data class to hold chart data 105 | 106 | ## [0.6.8] 107 | 108 | - Add report_title to context 109 | - Enhance SearchForm to be easier to override. Still needs more enhancements. 110 | 111 | ## [0.6.7] 112 | 113 | - Fix issue with `ReportField` when it has a `requires` in time series and crosstab reports 114 | 115 | ## [0.6.6] 116 | 117 | - Now a method on a generator can be effectively used as column 118 | - Use correct model when traversing on group by 119 | 120 | ## [0.6.5] 121 | 122 | - Fix Issue with group_by field pointing to model with custom primary key Issue #58 123 | 124 | ## [0.6.4] 125 | 126 | - Fix highchart cache to target the specific chart 127 | - Added initial and required to report_form_factory 128 | - Added base_q_filters and base_kwargs_filters to SlickReportField to control the base queryset 129 | - Add ability to customize ReportField on the fly 130 | - Adds `prevent_group_by` option to SlickReportField Will prevent group by calculation for this specific field, serves 131 | when you want to compute overall results. 132 | - Support reference to SlickReportField class directly in `requires` instead of its "registered" name. 133 | - Adds PercentageToBalance report field 134 | 135 | ## [0.6.3] 136 | 137 | - Change the deprecated in Django 4 `request.is_ajax` . 138 | 139 | ## [0.6.2] 140 | 141 | - Fix an issue with time series calculating first day of the month to be of the previous month #46 142 | 143 | ## [0.6.1] 144 | 145 | - Fix Django 4 compatibility (@squio) 146 | 147 | ## [0.6.0] 148 | 149 | - Breaking [ONLY] if you have overridden ReportView.get_report_results() 150 | - Moved the collecting of total report data to the report generator to make easier low level usage. 151 | - Fixed an issue with Charts.js `get_row_data` 152 | - Added ChartsOption 'time_series_support',in both chart.js and highcharts 153 | - Fixed `SlickReportField.create` to use the issuing class not the vanilla one. 154 | 155 | ## [0.5.8] 156 | 157 | - Fix compatibility with Django 3.2 158 | 159 | ## [0.5.7] 160 | 161 | - Add ability to refer to related fields in a group by report(@jrutila) 162 | 163 | ## [0.5.6] 164 | 165 | - Add exclude_field to report_form_factory (@gr4n0t4) 166 | - Added support for group by Many To Many field (@gr4n0t4) 167 | 168 | ## [0.5.5] 169 | 170 | - Add datepicker initialization function call (@squio) 171 | - Fixed an issue with default dates not being functional. 172 | 173 | ## [0.5.4] 174 | 175 | - Added missing prefix on integrity hash (@squio) 176 | 177 | ## [0.5.3] 178 | 179 | - Enhanced Field prepare flow 180 | - Add traversing for group_by 181 | - Allowed tests to run specific tests instead of the whole suit 182 | - Enhanced templates structure for easier override/customization 183 | 184 | ## [0.5.2] 185 | 186 | - Enhanced Time Series Plot total HighChart by accenting the categories 187 | - Enhanced the default verbose names of time series. 188 | - Expanding test coverage 189 | 190 | ## [0.5.1] 191 | 192 | - Allow for time series to operate on a non-group by report 193 | - Allow setting time series custom dates on ReportGenerator attr and init 194 | - Fix a bug with setting the queryset (but not the report model) on SlickReportView 195 | - Fixed an issue if GenericForeignKey is on the report model 196 | - Fixed an issue with Time series annual pattern 197 | 198 | ## [0.5.0] - 2020-12-11 199 | 200 | - Created the demo site https://django-slick-reporting.com/ 201 | - Add support to group by date field 202 | - Add `format_row` hook to SlickReportingView 203 | - Add support for several chart engine per same report 204 | - Add `SLICK_REPORTING_FORM_MEDIA` &`SLICK_REPORTING_DEFAULT_CHARTS_ENGINE` setting. 205 | - Documenting SlickReportView response structure. 206 | - Fix issue with special column names `__time_series__` and `__crosstab__` 207 | - Fix issue with Crosstab reminder option. 208 | 209 | ## [0.4.2] - 2020-11-29 210 | 211 | - Properly initialize Datepicker (#12 @squio) 212 | - Use previous date-range for initialization if it exists 213 | 214 | ## [0.4.1] - 2020-11-26 215 | 216 | - Bring back calculateTotalOnObjectArray (#11) 217 | - Bypassing default ordering by when generating the report (#10) 218 | - Fix in dates in template and view 219 | 220 | ## [0.4.0] - 2020-11-24 [BREAKING] 221 | 222 | - Renamed `SampleReportView` to `SlickReportView` 223 | - Renamed `BaseReportField` to `SlickReportField` 224 | - Added `SlickReportViewBase` leaving sanity checks for the `SlickReportView` 225 | 226 | ## [0.3.0] - 2020-11-23 227 | 228 | - Add Sanity checks against incorrect entries in columns or date_field 229 | - Add support to create ReportField on the fly in all report types 230 | - Enhance exception verbosity. 231 | - Removed `doc_date` field reference . 232 | 233 | ## [0.2.9] - 2020-10-22 234 | 235 | ### Updated 236 | 237 | - Fixed an issue getting a db field verbose column name 238 | - Fixed an issue with the report demo page's filter button not working correctly. 239 | 240 | ## [0.2.8] - 2020-10-05 241 | 242 | ### Updated 243 | 244 | - Fixed an error with ManyToOne Relation not being able to get its verbose name (@mswastik) 245 | 246 | ## [0.2.7] - 2020-07-24 247 | 248 | ### Updates 249 | 250 | - Bring back crosstab capability 251 | - Rename `quan` to the more verbose `quantity` 252 | - Minor enhancements around templates 253 | 254 | ## [0.2.6] - 2020-06-06 255 | 256 | ### Added 257 | 258 | - Adds `is_summable` option for ReportFields, and pass it to response 259 | - Add option to override a report fields while registering it. 260 | - Test ajax Request 261 | 262 | ### Updates and fixes 263 | 264 | - Fix a bug with time series adding one extra period. 265 | - Fix a bug with Crosstab data not passed to `report_form_factory` 266 | - Enhance Time series default column verbose name 267 | - testing: brought back ReportField after unregister test 268 | - Fix Pypi package not including statics. 269 | 270 | ## [0.2.5] - 2020-06-04 271 | 272 | ### Added 273 | 274 | - Crosstab support 275 | - Chart title defaults to report_title 276 | - Enhance fields naming 277 | 278 | ## [0.2.4] - 2020-05-27 279 | 280 | ### Added 281 | 282 | - Fix a naming issue with license (@iLoveTux) 283 | 284 | ## [0.2.3] - 2020-05-13 285 | 286 | ### Added 287 | 288 | - Ability to create a ReportField on the fly. 289 | - Document SLICK_REPORTING_DEFAULT_START_DATE & SLICK_REPORTING_DEFAULT_START_DATE settings 290 | - Test Report Field Registry 291 | - Lift the assumption that a Report field name should start and end with "__". This is only a convention now. 292 | 293 | ## [0.2.2] - 2020-04-26 294 | 295 | - Port Charting from [Ra Framework](https://github.com/ra-systems/RA) 296 | - Enhance ReportView HTML response 297 | 298 | ## [0.0.1] - 2020-04-24 299 | 300 | ### Added 301 | 302 | - Ported from [Ra Framework](https://github.com/ra-systems/RA) 303 | --------------------------------------------------------------------------------