[
  {
    "path": ".github/workflows/django.yml",
    "content": "name: Django CI\n\non:\n  push:\n    branches: [ \"develop\", \"master\" ]\n  pull_request:\n    branches: [ \"develop\", \"master\" ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      max-parallel: 4\n      matrix:\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r tests/requirements.txt\n\n      - name: Lint (ruff)\n        run: |\n          pip install ruff\n          ruff check --line-length 120 slick_reporting/\n\n      - name: Run tests\n        run: python runtests.py\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    environment: release\n    permissions:\n      contents: write   # GitHub Release + push back to develop\n      id-token: write   # PyPI OIDC trusted publishing\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0   # full history needed for the merge-back step\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r tests/requirements.txt\n\n      - name: Run tests\n        run: python runtests.py\n\n      - name: Install build tools\n        run: pip install build\n\n      - name: Build package\n        run: python -m build\n\n      - name: Publish to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n\n      - name: Extract release notes from CHANGELOG\n        id: changelog\n        run: |\n          TAG=${GITHUB_REF#refs/tags/v}\n          python scripts/extract_changelog.py \"$TAG\" > release_notes.md\n          echo \"tag=$TAG\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Create GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          body_path: release_notes.md\n          files: dist/*\n\n      - name: Notify demo server\n        env:\n          DEMO_WEBHOOK_URL: ${{ secrets.DEMO_WEBHOOK_URL }}\n          DEMO_WEBHOOK_SECRET: ${{ secrets.DEMO_WEBHOOK_SECRET }}\n        run: |\n          curl -fsS -X POST \"$DEMO_WEBHOOK_URL\" \\\n               -H \"Authorization: Bearer $DEMO_WEBHOOK_SECRET\" \\\n               -H \"Content-Type: application/json\" \\\n               -d '{\"version\": \"${{ steps.changelog.outputs.tag }}\"}'\n\n      - name: Merge master back into develop\n        run: |\n          git config user.name  \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n          git checkout develop\n          git merge master --no-edit\n          git push origin develop\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\nfabfile.py\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n\n-   repo: https://github.com/adamchainz/blacken-docs\n    rev: \"1.13.0\"\n    hooks:\n    -   id: blacken-docs\n        additional_dependencies:\n        - black==22.12.0\n\n-   repo: https://github.com/psf/black\n    rev: 23.3.0\n    hooks:\n    - id: black\n      language_version: python3.9\n\n- repo: https://github.com/astral-sh/ruff-pre-commit\n  # Ruff version.\n  rev: v0.0.287\n  hooks:\n    - id: ruff\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\n\nbuild:\n  os: \"ubuntu-22.04\"\n  tools:\n    python: \"3.11\"\n\n# Build from the docs/ directory with Sphinx\nsphinx:\n  configuration: docs/source/conf.py\n\n# Explicitly set the version of Python and its requirements\npython:\n  install:\n    - requirements: docs/requirements.txt"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [1.4.0] - 2026-05-01\n\n### New Features\n\n- **Dynamic Model support** — generate reports from any database table without defining a Django model.\n  New ``get_dynamic_model(table_name, database, schema)`` utility introspects a live table and returns a\n  fully usable (unmanaged) Django model. Supports 20+ field types and PostgreSQL schemas\n  (``schema`` parameter generates ``\"schema\".\"table_name\"`` as the db table).\n  Models are cached after first introspection.\n- **``table_name`` attribute on ``ReportGenerator`` and ``ReportView``** — shorthand for using a dynamic model.\n  Setting ``table_name = \"my_table\"`` is equivalent to setting ``report_model = get_dynamic_model(\"my_table\")``.\n  Column validation is deferred to request time for ``table_name``-based views so imports do not trigger\n  database access.\n- **Pre-computed crosstab reports** — new ``crosstab_precomputed = True`` flag on ``ReportGenerator``\n  switches from live aggregation to reading existing column values and pivoting them.\n  Use ``crosstab_columns`` to name the value columns and ``crosstab_field`` to identify the pivot field.\n  Distinct values for ``crosstab_field`` are auto-discovered from the database if ``crosstab_ids`` is not set.\n  Intended for materialized views, data-warehouse fact tables, and pre-aggregated ETL outputs.\n- **Print / HTML export** — new ``PrintHTMLExport`` class renders a clean, RTL-aware, print-ready HTML\n  page and triggers ``window.print()`` automatically. Enabled by default on all ``ReportView`` subclasses\n  via the new ``print_export_class`` attribute. The print export button opens in a new browser tab.\n  Templates are split into ``print_report.html``, ``print_report_header.html``, ``print_report_footer.html``,\n  and ``print_report_controls.html`` for easy overriding.\n- **Arabic (ar) and German (de) translations** added.\n\n### Improvements\n\n- **Static asset path resolution** — template tags now consistently apply Django's ``static()`` to any\n  relative URL in settings (jQuery URL, chart engine JS files, Font Awesome CSS URL). The\n  ``slick_reporting_settings`` JSON block written to the page also contains fully resolved static URLs,\n  ensuring correct paths with ``ManifestStaticFilesStorage`` or CDN-backed storages.\n- **Export action metadata** — ``get_export_actions()`` now includes a ``new_window`` flag per action;\n  the JS export handler opens a new tab when the flag is set, used by the print export.\n- **Report form extracted** — the filter form markup is now in its own ``report_form.html`` template\n  (included by ``report.html``) making it easier to override just the form section.\n- **Select2 removed from core assets** — Select2 was a demo-only dependency that made it into the default\n  ``SLICK_REPORTING_SETTINGS[\"MEDIA\"]`` JS/CSS lists and was auto-initialised in ``report_loader.js``.\n  It is now removed from the library defaults; demo project configures it separately.\n- **``fkeys_filter_func_hook``** — new static method on ``ReportViewBase`` that receives the dict of\n  detected foreign-key fields before the filter form is built. Override it to exclude or rename fields\n  (e.g. remove internal FK columns such as ``polymorphic_ctype_id``).\n- **Chart error recovery** — chart rendering errors are now caught in ``report_loader.js`` and displayed\n  as an inline message instead of silently breaking the rest of the page.\n\n### Bug Fixes\n\n- Fix ``crosstab_compute_remainder`` class-level attribute being ignored; the value was always read from\n  the request instead of falling back to the class default.\n\n### Chart Engine Fixes\n\n- Update Highcharts wrapper to use the modern ``Highcharts.chart()`` API (v11+), replacing the removed\n  jQuery plugin syntax. Switch default Highcharts CDN to jsDelivr (``cdn.jsdelivr.net/npm/highcharts@11``).\n- Fix Chart.js wrapper: corrected inverted ``is_time_series`` check; update to Chart.js v3/v4 API\n  (``plugins.title``, ``plugins.tooltip``, ``scales.y`` / ``scales.x``).\n- Add crosstab support to the Chart.js wrapper.\n- Fix Chart.js ``area`` chart type to render as ``line`` with ``fill: true``.\n- Fix Chart.js pie chart on time-series reports: automatically enables ``plot_total``.\n- Fix Chart.js pie chart sizing with ``aspectRatio: 2``.\n- Fix Chart.js chart cache key to include the element xpath, preventing chart destruction when\n  the same report appears multiple times on a dashboard.\n\n## [1.3.1] - 2024-06-16\n- Fix issue with Line Chart on highcharts engine\n- Reintroduce the stacking option on highcharts engine.\n- Fix issue with having different version of the same chart on the same page.\n- Enhanced the demo dashboard to show more capabilities regarding the charts.\n\n## [1.3.0] - 2023-11-08\n- Implement Slick reporting media override feature + docs\n- Add `Integrating reports into your Admin site` section to the docs\n- Group by and crosstab reports do not need date_field set anymore. Only time series do.\n- Fix in FirstBalance Computation field if no date is supplied\n- Add `REPORT_VIEW_ACCESS_FUNCTION` to control access to the report view\n\n\n## [1.2.0] - 2023-10-10\n- Add ``get_slick_reporting_media`` and ``get_charts_media`` templatetags\n- Add `get_group_by_custom_querysets` hook to ReportView\n- Enhance and document adding export options and customizing the builtin export to csv button\n- Enhance and document adding custom buttons to the report page\n- Enhance and document adding a new chart engine\n- Fix in SlickReportingListView\n- Move all css and js resources to be handled by `Media` governed by `settings.SLICK_REPORTING_SETTINGS`\n\n \n## [1.1.1] - 2023-09-25\n- Change settings to be a dict , adding support JQUERY_URL and FONT AWESOME customization #79 & #81\n- Fix issue with chartjs not being loaded  #80\n- Remove `SLICK_REPORTING_FORM_MEDIA`\n\n## [1.1.0] -\n- Breaking: changed ``report_title_context_key`` default value to `report_title`\n- Breaking: Renamed simple_report.html to report.html\n- Breaking: Renamed ``SlickReportField`` to ``ComputationField``. SlickReportField will continue to work till next release.\n- Revised and renamed js files\n- Add dashboard capabilities.\n- Added auto_load option to ReportView\n- Unified report loading to use the report loader\n- Fix issue with group_by_custom_queryset with time series\n- Fix issue with No group by report \n- Fix issue with traversing fields not showing up on ListViewReport\n- Fix issue with date filter not being respected in ListViewReport\n\n## [1.0.2] - 2023-08-31\n- Add a demo project for exploration and also containing all documentation code for proofing.\n- Revise and Enhancing Tutorial , Group by and Time series documentation.\n- Fix issue with error on dev console on report page due to resources duplication\n- Fix issue with Custom querysets not being correctly connected in the view\n- Fix issue with time series custom dates\n- Fix issue with Crosstab on traversing fields\n\n\n## [1.0.1] - 2023-07-03\n\n- Added missing js files ported from erp_framework package. \n- Document the need for \"crispy_bootstrap4\" in the docs and add it as a dependency in the setup.\n\n## [1.0.0] - 2023-07-03\n\n- Added crosstab_ids_custom_filters to allow custom filters on crosstab ids\n- Added ``group_by_custom_querysets`` to allow custom querysets as group \n- Added ability to have crosstab report in a time series report\n- Enhanced Docs content and structure.\n\n## [0.9.0] - 2023-06-07\n\n- Deprecated ``form_factory`` in favor of ``forms``, to be removed next version.\n- Deprecated `crosstab_model` in favor of ``crosstab_field``, to be removed next version.\n- 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.\n- Allowed cross tab on fields other than ForeignKey\n- Added support for start_date_field_name and end_date_field_name\n- Added support to crosstab on traversing fields\n- Added support for document types / debit and credit calculations\n- Added support for ordering via ``ReportView.default_order_by`` and/or passing the parameter ``order_by`` to the view\n- Added return of Ajax response in case of error and request is Ajax\n- Made it easy override to the search form. Create you own form and subclass BaseReportForm and implement the mandatory method(s).\n- Consolidated the needed resources in ``slick_reporting/js_resource.html`` template, so to use your own template you just need to include it.\n- Fixed an issue with report fields not respecting the queryset on the ReportView.\n- Fixed an issue if a foreign key have a custom `to_field` set either in ``group_by`` and/or `crosstab_field` .\n- Enhancing and adding to the documentation.\n- Black format the code and the documentation\n\n\n## [0.8.0]\n\n- Breaking: [Only if you use Crosstab reports] renamed crosstab_compute_reminder to crosstab_compute_remainder\n- Breaking : [Only if you set the templates statics by hand] renamed slick_reporting to ra.hightchart.js and ra.chartjs.js to \n  erp_framework.highchart.js and erp_framework.chartjs.js respectively\n- Fix an issue with Crosstab when there crosstab_compute_remainder = False\n\n## [0.7.0]\n\n- Added SlickReportingListView: a Report Class to display content of the model (like a ModelAdmin ChangeList)\n- Added `show_time_series_selector` capability to SlickReportView allowing User to change the time series pattern from\n  the UI.\n- Added ability to export to CSV from UI, using `ExportToStreamingCSV` & `ExportToCSV`\n- Now you can have a custom column defined on the SlickReportView (and not needing to customise the report generator).\n- You don't need to set date_field if you don't have calculations on the report\n- Easier customization of the crispy form layout\n- Enhance weekly time series default column name\n- Add `Chart` data class to hold chart data\n\n## [0.6.8]\n\n- Add report_title to context\n- Enhance SearchForm to be easier to override. Still needs more enhancements.\n\n## [0.6.7]\n\n- Fix issue with `ReportField` when it has a `requires` in time series and crosstab reports\n\n## [0.6.6]\n\n- Now a method on a generator can be effectively used as column\n- Use correct model when traversing on group by\n\n## [0.6.5]\n\n- Fix Issue with group_by field pointing to model with custom primary key Issue #58\n\n## [0.6.4]\n\n- Fix highchart cache to target the specific chart\n- Added initial and required to report_form_factory\n- Added base_q_filters and base_kwargs_filters to SlickReportField to control the base queryset\n- Add ability to customize ReportField on the fly\n- Adds `prevent_group_by` option to SlickReportField Will prevent group by calculation for this specific field, serves\n  when you want to compute overall results.\n- Support reference to SlickReportField class directly in `requires` instead of its \"registered\" name.\n- Adds PercentageToBalance report field\n\n## [0.6.3]\n\n- Change the deprecated in Django 4 `request.is_ajax` .\n\n## [0.6.2]\n\n- Fix an issue with time series calculating first day of the month to be of the previous month #46\n\n## [0.6.1]\n\n- Fix Django 4 compatibility (@squio)\n\n## [0.6.0]\n\n- Breaking [ONLY] if you have overridden ReportView.get_report_results()\n- Moved the collecting of total report data to the report generator to make easier low level usage.\n- Fixed an issue with Charts.js `get_row_data`\n- Added ChartsOption 'time_series_support',in both chart.js and highcharts\n- Fixed `SlickReportField.create` to use the issuing class not the vanilla one.\n\n## [0.5.8]\n\n- Fix compatibility with Django 3.2\n\n## [0.5.7]\n\n- Add ability to refer to related fields in a group by report(@jrutila)\n\n## [0.5.6]\n\n- Add exclude_field to report_form_factory (@gr4n0t4)\n- Added support for group by Many To Many field (@gr4n0t4)\n\n## [0.5.5]\n\n- Add datepicker initialization function call (@squio)\n- Fixed an issue with default dates not being functional.\n\n## [0.5.4]\n\n- Added missing prefix on integrity hash (@squio)\n\n## [0.5.3]\n\n- Enhanced Field prepare flow\n- Add traversing for group_by\n- Allowed tests to run specific tests instead of the whole suit\n- Enhanced templates structure for easier override/customization\n\n## [0.5.2]\n\n- Enhanced Time Series Plot total HighChart by accenting the categories\n- Enhanced the default verbose names of time series.\n- Expanding test coverage\n\n## [0.5.1]\n\n- Allow for time series to operate on a non-group by report\n- Allow setting time series custom dates on ReportGenerator attr and init\n- Fix a bug with setting the queryset (but not the report model) on SlickReportView\n- Fixed an issue if GenericForeignKey is on the report model\n- Fixed an issue with Time series annual pattern\n\n## [0.5.0] - 2020-12-11\n\n- Created the demo site https://django-slick-reporting.com/\n- Add support to group by date field\n- Add `format_row` hook to SlickReportingView\n- Add support for several chart engine per same report\n- Add `SLICK_REPORTING_FORM_MEDIA` &`SLICK_REPORTING_DEFAULT_CHARTS_ENGINE` setting.\n- Documenting SlickReportView response structure.\n- Fix issue with special column names `__time_series__` and `__crosstab__`\n- Fix issue with Crosstab reminder option.\n\n## [0.4.2] - 2020-11-29\n\n- Properly initialize Datepicker (#12 @squio)\n- Use previous date-range for initialization if it exists\n\n## [0.4.1] - 2020-11-26\n\n- Bring back calculateTotalOnObjectArray (#11)\n- Bypassing default ordering by when generating the report (#10)\n- Fix in dates in template and view\n\n## [0.4.0] - 2020-11-24 [BREAKING]\n\n- Renamed `SampleReportView` to `SlickReportView`\n- Renamed `BaseReportField` to `SlickReportField`\n- Added `SlickReportViewBase` leaving sanity checks for the `SlickReportView`\n\n## [0.3.0] - 2020-11-23\n\n- Add Sanity checks against incorrect entries in columns or date_field\n- Add support to create ReportField on the fly in all report types\n- Enhance exception verbosity.\n- Removed `doc_date` field reference .\n\n## [0.2.9] - 2020-10-22\n\n### Updated\n\n- Fixed an issue getting a db field verbose column name\n- Fixed an issue with the report demo page's filter button not working correctly.\n\n## [0.2.8] - 2020-10-05\n\n### Updated\n\n- Fixed an error with ManyToOne Relation not being able to get its verbose name (@mswastik)\n\n## [0.2.7] - 2020-07-24\n\n### Updates\n\n- Bring back crosstab capability\n- Rename `quan` to the more verbose `quantity`\n- Minor enhancements around templates\n\n## [0.2.6] - 2020-06-06\n\n### Added\n\n- Adds `is_summable` option for ReportFields, and pass it to response\n- Add option to override a report fields while registering it.\n- Test ajax Request\n\n### Updates and fixes\n\n- Fix a bug with time series adding one extra period.\n- Fix a bug with Crosstab data not passed to `report_form_factory`\n- Enhance Time series default column verbose name\n- testing: brought back ReportField after unregister test\n- Fix Pypi package not including statics.\n\n## [0.2.5] - 2020-06-04\n\n### Added\n\n- Crosstab support\n- Chart title defaults to report_title\n- Enhance fields naming\n\n## [0.2.4] - 2020-05-27\n\n### Added\n\n- Fix a naming issue with license (@iLoveTux)\n\n## [0.2.3] - 2020-05-13\n\n### Added\n\n- Ability to create a ReportField on the fly.\n- Document SLICK_REPORTING_DEFAULT_START_DATE & SLICK_REPORTING_DEFAULT_START_DATE settings\n- Test Report Field Registry\n- Lift the assumption that a Report field name should start and end with \"__\". This is only a convention now.\n\n## [0.2.2] - 2020-04-26\n\n- Port Charting from [Ra Framework](https://github.com/ra-systems/RA)\n- Enhance ReportView HTML response\n\n## [0.0.1] - 2020-04-24\n\n### Added\n\n- Ported from [Ra Framework](https://github.com/ra-systems/RA) \n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project\n\nDjango Slick Reporting is a Django library for generating analytical reports: simple aggregations, group-by, time-series, and crosstab (matrix) reports with built-in chart support (Highcharts, Chart.js).\n\n## Commands\n\n### Run all tests\n```bash\npython runtests.py\n```\n\n### Run a specific test\n```bash\npython runtests.py tests.tests.TestClassName.test_method_name\n```\n\n### Run tests with coverage\n```bash\ncoverage run --include=../* runtests.py && coverage html\n```\n\n### Format & lint\n```bash\nblack --line-length 120 .\nruff check --line-length 120 .\n```\n\n## Architecture\n\nThe library is layered as: **ReportView** (Django CBV) → **ReportGenerator** (computation engine) → **ComputationField** (calculation definitions) → **ReportFieldRegistry** (field lookup).\n\n### Key modules in `slick_reporting/`\n\n- **generator.py** — Core engine. `ReportGenerator` handles group-by, time-series, and crosstab logic. `ListViewReportGenerator` handles ungrouped row-level reports. Entry point is `get_report_data()`.\n- **fields.py** — `ComputationField` base class and built-ins (`TotalReportField`, `BalanceReportField`, etc.). Fields declare `calculation_method` (e.g. `Sum`), `calculation_field`, and `requires` for dependency chaining. Use `ComputationField.create()` factory or subclass + `@report_field_register` decorator.\n- **views.py** — `ReportView` extends `FormView` with report generation, chart context, CSV export, and AJAX support. Access control via `test_func()`.\n- **forms.py** — `ReportForm` auto-generates filter forms from model ForeignKeys with crispy-forms/Bootstrap layout. `report_form_factory` builds forms dynamically.\n- **registry.py** — `field_registry` singleton. ComputationFields self-register to avoid naming collisions with factory-created fields.\n- **app_settings.py** — Defaults and settings loaded from Django's `SLICK_REPORTING_SETTINGS` dict.\n\n### Charts\n\nCharts are configurable per report via `Chart` dataclass. Supported engines: Highcharts, Chart.js, and Apex Charts. New chart engines can be added by providing a JS integration file and registering it in settings.\n\n### Report types\n\nControlled by `ReportGenerator` configuration:\n- **Group-by**: set `group_by` field, get one row per distinct value\n- **Time-series**: set `time_series_pattern` (daily/weekly/monthly/yearly/custom), columns repeat per period\n- **Crosstab**: set `crosstab_field` + `crosstab_ids`, produces matrix layout\n- **Crosstab + Time-series**: can be combined for a matrix over time periods\n- **List view**: use `ListViewReportGenerator` for ungrouped row-level output\n\n### Column duck typing\n\nColumns in `columns` list can be: model field names (str), traversing field names on the group-by model (e.g. `\"client__contact__name\"`), `ComputationField` subclasses, or special markers (`__total__`, `__balance__`, `__time_series__`, `__crosstab__`).\n\n## Test structure\n\nTests live in `tests/` with settings in `tests/settings.py` (SQLite, no migrations). Test models (`Product`, `Client`, `SimpleSales`, etc.) are defined in `tests/models.py`. `BaseTestData` in `tests/tests.py` creates fixture data across multiple dates for time-series testing.\n\n## Code style\n\n- Black + Ruff, line length 120\n- CI runs on Python 3.9, 3.10, 3.11\n"
  },
  {
    "path": "LICENSE.md",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2020, Ra Systems\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE.md\ninclude README.md\nrecursive-include slick_reporting/static *\nrecursive-include slick_reporting/templates *\nrecursive-include slick_reporting/locale *\nrecursive-exclude tests/ *"
  },
  {
    "path": "README.rst",
    "content": ".. image:: https://img.shields.io/pypi/v/django-slick-reporting.svg\n    :target: https://pypi.org/project/django-slick-reporting\n\n.. image:: https://img.shields.io/pypi/pyversions/django-slick-reporting.svg\n    :target: https://pypi.org/project/django-slick-reporting\n\n.. image:: https://img.shields.io/readthedocs/django-slick-reporting\n    :target: https://django-slick-reporting.readthedocs.io/\n\n.. image:: https://api.travis-ci.com/ra-systems/django-slick-reporting.svg?branch=master\n    :target: https://app.travis-ci.com/github/ra-systems/django-slick-reporting\n\n.. image:: https://img.shields.io/codecov/c/github/ra-systems/django-slick-reporting\n    :target: https://codecov.io/gh/ra-systems/django-slick-reporting\n\n\n\n\nDjango Slick Reporting\n======================\n\nA one stop reports engine with batteries included.\n\nFeatures\n--------\n\n- Effortlessly create Simple, Grouped, Time series and Crosstab reports in a handful of code lines.\n- Create Chart(s) for your reports with a single line of code.\n- Create Custom complex Calculation.\n- Optimized for speed.\n- Easily extendable.\n\nInstallation\n------------\n\nUse the package manager `pip <https://pip.pypa.io/en/stable/>`_ to install django-slick-reporting.\n\n.. code-block:: console\n\n        pip install django-slick-reporting\n\n\nUsage\n-----\n\nSo we have a model `SalesTransaction` which contains typical data about a sale.\nWe can extract different kinds of information for that model.\n\nLet'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.\n\n.. code-block:: python\n\n\n    # in views.py\n    from django.db.models import Sum\n    from slick_reporting.views import ReportView, Chart\n    from slick_reporting.fields import ComputationField\n    from .models import MySalesItems\n\n\n    class TotalProductSales(ReportView):\n        report_model = SalesTransaction\n        date_field = \"date\"\n        group_by = \"product\"\n        columns = [\n            \"name\",\n            ComputationField.create(\n                Sum, \"quantity\", verbose_name=\"Total quantity sold\", is_summable=False\n            ),\n            ComputationField.create(\n                Sum, \"value\", name=\"sum__value\", verbose_name=\"Total Value sold $\"\n            ),\n        ]\n\n        chart_settings = [\n            Chart(\n                \"Total sold $\",\n                Chart.BAR,\n                data_source=[\"sum__value\"],\n                title_source=[\"name\"],\n            ),\n            Chart(\n                \"Total sold $ [PIE]\",\n                Chart.PIE,\n                data_source=[\"sum__value\"],\n                title_source=[\"name\"],\n            ),\n        ]\n\n\n    # then, in urls.py\n    path(\"total-sales-report\", TotalProductSales.as_view())\n\n\n\nWith this code, you will get something like this:\n\n.. image:: https://i.ibb.co/SvxTM23/Selection-294.png\n    :target: https://i.ibb.co/SvxTM23/Selection-294.png\n    :alt: Shipped in View Page\n\n\nTime Series\n-----------\n\nA Time series report is a report that is generated for a periods of time.\nThe period can be daily, weekly, monthly, yearly or custom. Calculations will be performed for each period in the time series.\n\nExample: How much was sold in value for each product monthly within a date period ?\n\n.. code-block:: python\n\n    # in views.py\n    from slick_reporting.views import ReportView\n    from slick_reporting.fields import ComputationField\n    from .models import SalesTransaction\n\n\n    class MonthlyProductSales(ReportView):\n        report_model = SalesTransaction\n        date_field = \"date\"\n        group_by = \"product\"\n        columns = [\"name\", \"sku\"]\n\n        time_series_pattern = \"monthly\"\n        # or \"yearly\" , \"weekly\" , \"daily\" , others and custom patterns\n        time_series_columns = [\n            ComputationField.create(\n                Sum, \"value\", verbose_name=_(\"Sales Value\"), name=\"value\"\n            )  # what will be calculated for each month\n        ]\n\n        chart_settings = [\n            Chart(\n                _(\"Total Sales Monthly\"),\n                Chart.PIE,\n                data_source=[\"value\"],\n                title_source=[\"name\"],\n                plot_total=True,\n            ),\n            Chart(\n                \"Total Sales [Area chart]\",\n                Chart.AREA,\n                data_source=[\"value\"],\n                title_source=[\"name\"],\n                plot_total=False,\n            ),\n        ]\n\n\n.. image:: https://github.com/ra-systems/django-slick-reporting/blob/develop/docs/source/topics/_static/timeseries.png?raw=true\n    :alt: Time Series Report\n    :align: center\n\nCross Tab\n---------\nUse crosstab reports, also known as matrix reports, to show the relationships between three or more query items.\nCrosstab reports show data in rows and columns with information summarized at the intersection points.\n\n.. code-block:: python\n\n        # in views.py\n        from slick_reporting.views import ReportView\n        from slick_reporting.fields import ComputationField\n        from .models import MySalesItems\n\n\n        class MyCrosstabReport(ReportView):\n\n            crosstab_field = \"client\"\n            crosstab_ids = [1, 2, 3]\n            crosstab_columns = [\n                ComputationField.create(Sum, \"value\", verbose_name=_(\"Value for\")),\n            ]\n            crosstab_compute_remainder = True\n\n            columns = [\n                \"some_optional_field\",\n                # You can customize where the crosstab columns are displayed in relation to the other columns\n                \"__crosstab__\",\n                # 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\n                ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Value\")),\n            ]\n\n\n.. image:: https://github.com/ra-systems/django-slick-reporting/blob/develop/docs/source/topics/_static/crosstab.png?raw=true\n   :alt: Homepage\n   :align: center\n\n\nLow level\n---------\n\nThe view is a wrapper over the `ReportGenerator` class, which is the core of the reporting engine.\nYou can interact with the `ReportGenerator` using same syntax as used with the `ReportView` .\n\n.. code-block:: python\n\n    from slick_reporting.generator import ReportGenerator\n    from .models import MySalesModel\n\n\n    class MyReport(ReportGenerator):\n        report_model = MySalesModel\n        group_by = \"product\"\n        columns = [\"title\", \"__total__\"]\n\n\n    # OR\n    my_report = ReportGenerator(\n        report_model=MySalesModel, group_by=\"product\", columns=[\"title\", \"__total__\"]\n    )\n    my_report.get_report_data()  # -> [{'title':'Product 1', '__total__: 56}, {'title':'Product 2', '__total__: 43}, ]\n\n\nThis is just a scratch of what you can do and customize.\n\nDemo site\n---------\n\nAvailable on `Django Slick Reporting <https://django-slick-reporting.com/>`_\n\n\nYou can also use locally\n\n.. code-block:: console\n\n        # clone the repo\n        git clone https://github.com/ra-systems/django-slick-reporting.git\n        # create a virtual environment and activate it\n        python -m venv /path/to/new/virtual/environment\n        source /path/to/new/virtual/environment/bin/activate\n\n        cd django-slick-reporting/demo_proj\n        pip install -r requirements.txt\n        python manage.py migrate\n        python manage.py create_entries\n        python manage.py runserver\n\nthe ``create_entries`` command will generate data for the demo app\n\nDocumentation\n-------------\n\nAvailable on `Read The Docs <https://django-slick-reporting.readthedocs.io/en/latest/>`_\n\nYou can run documentation locally\n\n.. code-block:: console\n\n    <activate your virtual environment>\n    cd docs\n    pip install -r requirements.txt\n    sphinx-build -b html source build\n\n\nRoad Ahead\n----------\n\n* Continue on enriching the demo project\n* Add the dashboard capabilities\n\n\nRunning tests\n-----------------\nCreate a virtual environment (maybe with `virtual slick_reports_test`), activate it; Then ,\n \n.. code-block:: console\n    \n    $ git clone git+git@github.com:ra-systems/django-slick-reporting.git\n    $ cd tests\n    $ python -m pip install -e ..\n\n    $ python runtests.py\n    #     Or for Coverage report\n    $ coverage run --include=../* runtests.py [-k]\n    $ coverage html\n\n\nSupport & Contributing\n----------------------\n\nPlease consider star the project to keep an eye on it. Your PRs, reviews are most welcome and needed.\n\nWe honor the well formulated `Django's guidelines <https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/>`_ to serve as contribution guide here too.\n\n\nAuthors\n--------\n\n* **Ramez Ashraf** - *Initial work* - `RamezIssac <https://github.com/RamezIssac>`_\n\nCross Reference\n---------------\n\nIf you like this package, chances are you may like those packages too!\n\n`Django Tabular Permissions <https://github.com/RamezIssac/django-tabular-permissions>`_ Display Django permissions in a HTML table that is translatable and easy customized.\n\n`Django ERP Framework <https://github.com/ra-systems/RA>`_ A framework to build business solutions with ease.\n\nIf you find this project useful or promising , You can support us by a github ⭐\n"
  },
  {
    "path": "demo_proj/demo_app/__init__.py",
    "content": ""
  },
  {
    "path": "demo_proj/demo_app/admin.py",
    "content": "\n# Register your models here.\n"
  },
  {
    "path": "demo_proj/demo_app/apps.py",
    "content": "from django.apps import AppConfig\n\n\nclass DemoAppConfig(AppConfig):\n    default_auto_field = \"django.db.models.BigAutoField\"\n    name = \"demo_app\"\n"
  },
  {
    "path": "demo_proj/demo_app/forms.py",
    "content": "from django import forms\nfrom django.db.models import Q\nfrom slick_reporting.forms import BaseReportForm\n\n\nclass TotalSalesFilterForm(BaseReportForm, forms.Form):\n    PRODUCT_SIZE_CHOICES = (\n        (\"all\", \"All\"),\n        (\"big-only\", \"Big Only\"),\n        (\"small-only\", \"Small Only\"),\n        (\"medium-only\", \"Medium Only\"),\n        (\"all-except-extra-big\", \"All except extra Big\"),\n    )\n    start_date = forms.DateField(\n        required=False,\n        label=\"Start Date\",\n        widget=forms.DateInput(attrs={\"type\": \"date\"}),\n    )\n    end_date = forms.DateField(\n        required=False, label=\"End Date\", widget=forms.DateInput(attrs={\"type\": \"date\"})\n    )\n    product_size = forms.ChoiceField(\n        choices=PRODUCT_SIZE_CHOICES, required=False, label=\"Product Size\", initial=\"all\"\n    )\n\n    def get_filters(self):\n        # return the filters to be used in the report\n        # Note: the use of Q filters and kwargs filters\n        kw_filters = {}\n        q_filters = []\n        if self.cleaned_data[\"product_size\"] == \"big-only\":\n            kw_filters[\"product__size__in\"] = [\"extra_big\", \"big\"]\n        elif self.cleaned_data[\"product_size\"] == \"small-only\":\n            kw_filters[\"product__size__in\"] = [\"extra_small\", \"small\"]\n        elif self.cleaned_data[\"product_size\"] == \"medium-only\":\n            kw_filters[\"product__size__in\"] = [\"medium\"]\n        elif self.cleaned_data[\"product_size\"] == \"all-except-extra-big\":\n            q_filters.append(~Q(product__size__in=[\"extra_big\", \"big\"]))\n        return q_filters, kw_filters\n\n    def get_start_date(self):\n        return self.cleaned_data[\"start_date\"]\n\n    def get_end_date(self):\n        return self.cleaned_data[\"end_date\"]\n"
  },
  {
    "path": "demo_proj/demo_app/helpers.py",
    "content": "from django.urls import path\n\nfrom . import reports\n\nTUTORIAL = [\n    (\"product-sales\", reports.ProductSales),\n    (\"total-product-sales\", reports.TotalProductSales),\n    (\"total-product-sales-by-country\", reports.TotalProductSalesByCountry),\n    (\"monthly-product-sales\", reports.MonthlyProductSales),\n    (\"product-sales-per-client-crosstab\", reports.ProductSalesPerClientCrosstab),\n    (\"product-sales-per-country-crosstab\", reports.ProductSalesPerCountryCrosstab),\n    (\"last-10-sales\", reports.LastTenSales),\n    (\"total-product-sales-with-custom-form\", reports.TotalProductSalesWithCustomForm),\n]\n\nGROUP_BY = [\n    (\"group-by-report\", reports.GroupByReport),\n    (\"group-by-traversing-field\", reports.GroupByTraversingFieldReport),\n    (\"group-by-custom-queryset\", reports.GroupByCustomQueryset),\n    (\"no-group-by\", reports.NoGroupByReport),\n]\n\nTIME_SERIES = [\n    (\"time-series-report\", reports.TimeSeriesReport),\n    (\"time-series-with-selector\", reports.TimeSeriesReportWithSelector),\n    (\"time-series-with-custom-dates\", reports.TimeSeriesReportWithCustomDates),\n    (\"time-series-with-custom-dates-and-title\", reports.TimeSeriesReportWithCustomDatesAndCustomTitle),\n    (\"time-series-without-group-by\", reports.TimeSeriesWithoutGroupBy),\n    (\"time-series-with-group-by-custom-queryset\", reports.TimeSeriesReportWithCustomGroupByQueryset),\n]\n\nCROSSTAB = [\n    (\"crosstab-report\", reports.CrosstabReport),\n    (\"crosstab-report-with-ids\", reports.CrosstabWithIds),\n    (\"crosstab-report-traversing-field\", reports.CrosstabWithTraversingField),\n    (\"crosstab-report-custom-filter\", reports.CrosstabWithIdsCustomFilter),\n    (\"crosstab-report-custom-verbose-name\", reports.CrossTabReportWithCustomVerboseName),\n    (\"crosstab-report-custom-verbose-name-2\", reports.CrossTabReportWithCustomVerboseNameCustomFilter),\n    (\"crosstab-report-with-time-series\", reports.CrossTabWithTimeSeries),\n]\nPIVOT = [\n    (\"precomputed-monthly-sales\", reports.PreComputedMonthlySales),\n    (\"dynamic-model-sales-by-country\", reports.DynamicModelSalesByCountry),\n]\n\nOTHER = [\n    (\"highcharts-examples\", reports.HighChartExample),\n    (\"chartjs-examples\", reports.ChartJSExample),\n    (\"apexcharts-examples\", reports.ProductSalesApexChart),\n    (\"custom-export\", reports.CustomExportReport),\n    (\"form-initial\", reports.ReportWithFormInitial),\n]\n\n\ndef get_urls_patterns():\n    urls = []\n    for name, report in TUTORIAL + GROUP_BY + TIME_SERIES + CROSSTAB + PIVOT + OTHER:\n        urls.append(path(f\"{name}/\", report.as_view(), name=name))\n    return urls\n"
  },
  {
    "path": "demo_proj/demo_app/management/commands/create_entries.py",
    "content": "import datetime\nimport random\nfrom datetime import timedelta\n\nfrom django.contrib.auth import get_user_model\nfrom django.core.management.base import BaseCommand\n\n# from expense.models import Expense, ExpenseTransaction\nfrom ...models import Client, Product, SalesTransaction, ProductCategory, MonthlySalesSummary\n\nUser = get_user_model()\n\n\ndef date_range(start_date, end_date):\n    for i in range((end_date - start_date).days + 1):\n        yield start_date + timedelta(i)\n\n\nclass Command(BaseCommand):\n    help = \"Create Sample entries for the demo app\"\n\n    def handle(self, *args, **options):\n        # create clients\n        client_countries = [\n            \"US\",\n            \"DE\",\n            \"EG\",\n            \"IN\",\n            \"KW\",\n            \"RA\"\n        ]\n        product_category = [\n            \"extra_big\",\n            \"big\",\n            \"medium\",\n            \"small\",\n            \"extra-small\"\n        ]\n        SalesTransaction.objects.all().delete()\n        Client.objects.all().delete()\n        Product.objects.all().delete()\n        ProductCategory.objects.all().delete()\n        User.objects.filter(is_superuser=False).delete()\n        for i in range(10):\n            User.objects.create_user(username=f\"user {i}\", password=\"password\")\n\n        list(User.objects.values_list(\"id\", flat=True))\n        for i in range(1, 4):\n            ProductCategory.objects.create(name=f\"Product Category {i}\")\n\n        product_category_ids = list(ProductCategory.objects.values_list(\"id\", flat=True))\n        for i in range(1, 10):\n            Client.objects.create(name=f\"Client {i}\",\n                                  country=random.choice(client_countries),\n                                  # owner_id=random.choice(users_id)\n                                  )\n        clients_ids = list(Client.objects.values_list(\"pk\", flat=True))\n        # create products\n        for i in range(1, 10):\n            Product.objects.create(name=f\"Product {i}\",\n                                   product_category_id=random.choice(product_category_ids),\n                                   size=random.choice(product_category))\n        products_ids = list(Product.objects.values_list(\"pk\", flat=True))\n\n        current_year = datetime.datetime.today().year\n        start_date = datetime.datetime(current_year, 1, 1)\n        end_date = datetime.datetime(current_year + 1, 1, 1)\n\n        for date in date_range(start_date, end_date):\n            for i in range(1, 10):\n                SalesTransaction.objects.create(\n                    client_id=random.choice(clients_ids),\n                    product_id=random.choice(products_ids),\n                    quantity=random.randint(1, 10),\n                    price=random.randint(1, 100),\n                    date=date,\n                    number=f\"Sale {date.strftime('%Y-%m-%d')} #{i}\",\n                )\n                # ExpenseTransaction.objects.create(\n                #     expense_id=random.choice(expense_ids),\n                #     value=random.randint(1, 100),\n                #     date=date,\n                #     number=f\"Expense {date.strftime('%Y-%m-%d')} #{i}\",\n                # )\n\n        # Populate MonthlySalesSummary from the generated SalesTransaction data\n        MonthlySalesSummary.objects.all().delete()\n        from django.db.models.functions import TruncMonth\n        from django.db.models import Sum\n\n        monthly_data = (\n            SalesTransaction.objects.annotate(month=TruncMonth(\"date\"))\n            .values(\"product_id\", \"month\")\n            .annotate(total_sales=Sum(\"value\"), total_quantity=Sum(\"quantity\"))\n        )\n        for row in monthly_data:\n            MonthlySalesSummary.objects.create(\n                product_id=row[\"product_id\"],\n                month=row[\"month\"],\n                total_sales=row[\"total_sales\"],\n                total_quantity=row[\"total_quantity\"],\n            )\n\n        # Create a raw SQL table for the dynamic model demo\n        from django.db import connection\n\n        with connection.cursor() as cursor:\n            cursor.execute(\"DROP TABLE IF EXISTS regional_sales_summary\")\n            cursor.execute(\"\"\"\n                CREATE TABLE regional_sales_summary (\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    product_name VARCHAR(100) NOT NULL,\n                    country VARCHAR(100) NOT NULL,\n                    total_sales DECIMAL(12, 2) NOT NULL DEFAULT 0,\n                    total_quantity DECIMAL(12, 2) NOT NULL DEFAULT 0\n                )\n            \"\"\")\n            country_data = (\n                SalesTransaction.objects\n                .values(\"product__name\", \"client__country\")\n                .annotate(total_sales=Sum(\"value\"), total_quantity=Sum(\"quantity\"))\n            )\n            for row in country_data:\n                cursor.execute(\n                    \"INSERT INTO regional_sales_summary (product_name, country, total_sales, total_quantity) \"\n                    \"VALUES (%s, %s, %s, %s)\",\n                    [row[\"product__name\"], row[\"client__country\"], row[\"total_sales\"], row[\"total_quantity\"]],\n                )\n\n        self.stdout.write(self.style.SUCCESS(\"Entries Created Successfully\"))\n"
  },
  {
    "path": "demo_proj/demo_app/migrations/0001_initial.py",
    "content": "# Generated by Django 4.2 on 2023-08-02 09:14\n\nfrom django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n    initial = True\n\n    dependencies = []\n\n    operations = [\n        migrations.CreateModel(\n            name=\"Client\",\n            fields=[\n                (\n                    \"id\",\n                    models.BigAutoField(\n                        auto_created=True,\n                        primary_key=True,\n                        serialize=False,\n                        verbose_name=\"ID\",\n                    ),\n                ),\n                (\"name\", models.CharField(max_length=100, verbose_name=\"Client Name\")),\n            ],\n            options={\n                \"verbose_name\": \"Client\",\n                \"verbose_name_plural\": \"Clients\",\n            },\n        ),\n        migrations.CreateModel(\n            name=\"Product\",\n            fields=[\n                (\n                    \"id\",\n                    models.BigAutoField(\n                        auto_created=True,\n                        primary_key=True,\n                        serialize=False,\n                        verbose_name=\"ID\",\n                    ),\n                ),\n                (\"name\", models.CharField(max_length=100, verbose_name=\"Product Name\")),\n            ],\n            options={\n                \"verbose_name\": \"Product\",\n                \"verbose_name_plural\": \"Products\",\n            },\n        ),\n        migrations.CreateModel(\n            name=\"SalesTransaction\",\n            fields=[\n                (\n                    \"id\",\n                    models.BigAutoField(\n                        auto_created=True,\n                        primary_key=True,\n                        serialize=False,\n                        verbose_name=\"ID\",\n                    ),\n                ),\n                (\n                    \"number\",\n                    models.CharField(\n                        max_length=100, verbose_name=\"Sales Transaction #\"\n                    ),\n                ),\n                (\"date\", models.DateTimeField()),\n                (\"notes\", models.TextField(blank=True, null=True)),\n                (\"value\", models.DecimalField(decimal_places=2, max_digits=9)),\n                (\n                    \"client\",\n                    models.ForeignKey(\n                        on_delete=django.db.models.deletion.PROTECT,\n                        to=\"demo_app.client\",\n                        verbose_name=\"Client\",\n                    ),\n                ),\n                (\n                    \"product\",\n                    models.ForeignKey(\n                        on_delete=django.db.models.deletion.PROTECT,\n                        to=\"demo_app.product\",\n                        verbose_name=\"Product\",\n                    ),\n                ),\n            ],\n            options={\n                \"verbose_name\": \"Sales Transaction\",\n                \"verbose_name_plural\": \"Sales Transactions\",\n            },\n        ),\n    ]\n"
  },
  {
    "path": "demo_proj/demo_app/migrations/0002_salestransaction_price_salestransaction_quantity.py",
    "content": "# Generated by Django 4.2 on 2023-08-02 09:14\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n    dependencies = [\n        (\"demo_app\", \"0001_initial\"),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name=\"salestransaction\",\n            name=\"price\",\n            field=models.DecimalField(decimal_places=2, default=0, max_digits=9),\n            preserve_default=False,\n        ),\n        migrations.AddField(\n            model_name=\"salestransaction\",\n            name=\"quantity\",\n            field=models.DecimalField(decimal_places=2, default=0, max_digits=9),\n            preserve_default=False,\n        ),\n    ]\n"
  },
  {
    "path": "demo_proj/demo_app/migrations/0003_product_category.py",
    "content": "# Generated by Django 4.2 on 2023-08-30 08:06\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n    dependencies = [\n        (\"demo_app\", \"0002_salestransaction_price_salestransaction_quantity\"),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name=\"product\",\n            name=\"category\",\n            field=models.CharField(\n                default=\"Medium\", max_length=100, verbose_name=\"Product Category\"\n            ),\n        ),\n    ]\n"
  },
  {
    "path": "demo_proj/demo_app/migrations/0004_client_country_product_sku.py",
    "content": "# Generated by Django 4.2.4 on 2023-08-30 08:38\n\nfrom django.db import migrations, models\nimport uuid\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('demo_app', '0003_product_category'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='client',\n            name='country',\n            field=models.CharField(default='US', max_length=255, verbose_name='Country'),\n        ),\n        migrations.AddField(\n            model_name='product',\n            name='sku',\n            field=models.CharField(default=uuid.uuid4, max_length=255, verbose_name='SKU'),\n        ),\n    ]\n"
  },
  {
    "path": "demo_proj/demo_app/migrations/0005_product_size.py",
    "content": "# Generated by Django 4.2.4 on 2023-08-30 11:24\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('demo_app', '0004_client_country_product_sku'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='product',\n            name='size',\n            field=models.CharField(default='Medium', max_length=100, verbose_name='Product Category'),\n        ),\n    ]\n"
  },
  {
    "path": "demo_proj/demo_app/migrations/0006_productcategory_remove_product_category_and_more.py",
    "content": "# Generated by Django 4.2.4 on 2023-08-30 17:57\n\nfrom django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('demo_app', '0005_product_size'),\n    ]\n\n    operations = [\n        migrations.CreateModel(\n            name='ProductCategory',\n            fields=[\n                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('name', models.CharField(max_length=100, verbose_name='Product Category Name')),\n            ],\n        ),\n        migrations.RemoveField(\n            model_name='product',\n            name='category',\n        ),\n        migrations.AlterField(\n            model_name='product',\n            name='size',\n            field=models.CharField(default='Medium', max_length=100, verbose_name='Size'),\n        ),\n        migrations.AddField(\n            model_name='product',\n            name='product_category',\n            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='demo_app.productcategory'),\n        ),\n    ]\n"
  },
  {
    "path": "demo_proj/demo_app/migrations/0007_monthlysalessummary.py",
    "content": "# Generated by Django 6.0.3 on 2026-04-23 15:27\n\nimport django.db.models.deletion\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('demo_app', '0006_productcategory_remove_product_category_and_more'),\n    ]\n\n    operations = [\n        migrations.CreateModel(\n            name='MonthlySalesSummary',\n            fields=[\n                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('month', models.DateField(verbose_name='Month')),\n                ('total_sales', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Total Sales')),\n                ('total_quantity', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Total Quantity')),\n                ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='demo_app.product', verbose_name='Product')),\n            ],\n            options={\n                'verbose_name': 'Monthly Sales Summary',\n                'verbose_name_plural': 'Monthly Sales Summaries',\n                'unique_together': {('product', 'month')},\n            },\n        ),\n        migrations.RunSQL(\"\"\"\n        CREATE TABLE regional_sales_summary\n        (\n            id             INTEGER PRIMARY KEY AUTOINCREMENT,\n            product_name   VARCHAR(100)   NOT NULL,\n            country        VARCHAR(100)   NOT NULL,\n            total_sales    DECIMAL(12, 2) NOT NULL DEFAULT 0,\n            total_quantity DECIMAL(12, 2) NOT NULL DEFAULT 0\n        )\n        \"\"\")\n    ]\n"
  },
  {
    "path": "demo_proj/demo_app/migrations/__init__.py",
    "content": ""
  },
  {
    "path": "demo_proj/demo_app/models.py",
    "content": "import uuid\n\nfrom django.db import models\nfrom django.utils.translation import gettext_lazy as _\n\n\n# Create your models here.\nclass Client(models.Model):\n    name = models.CharField(max_length=100, verbose_name=\"Client Name\")\n    country = models.CharField(_(\"Country\"), max_length=255, default=\"US\")\n\n    class Meta:\n        verbose_name = _(\"Client\")\n        verbose_name_plural = _(\"Clients\")\n\n    def __str__(self):\n        return self.name\n\n\nclass ProductCategory(models.Model):\n    name = models.CharField(max_length=100, verbose_name=\"Product Category Name\")\n\n    def __str__(self):\n        return self.name\n\n\nclass Product(models.Model):\n    name = models.CharField(max_length=100, verbose_name=\"Product Name\")\n    # category = models.CharField(max_length=100, verbose_name=\"Product Category\", default=\"Medium\")\n    product_category = models.ForeignKey(ProductCategory, on_delete=models.CASCADE, null=True)\n\n    sku = models.CharField(_(\"SKU\"), max_length=255, default=uuid.uuid4)\n    size = models.CharField(max_length=100, verbose_name=\"Size\", default=\"Medium\")\n\n    class Meta:\n        verbose_name = _(\"Product\")\n        verbose_name_plural = _(\"Products\")\n\n    def __str__(self):\n        return self.name\n\n\nclass SalesTransaction(models.Model):\n    number = models.CharField(max_length=100, verbose_name=\"Sales Transaction #\")\n    date = models.DateTimeField()\n    notes = models.TextField(blank=True, null=True)\n    client = models.ForeignKey(\n        Client, on_delete=models.PROTECT, verbose_name=_(\"Client\")\n    )\n    product = models.ForeignKey(\n        Product, on_delete=models.PROTECT, verbose_name=_(\"Product\")\n    )\n    value = models.DecimalField(max_digits=9, decimal_places=2)\n    quantity = models.DecimalField(max_digits=9, decimal_places=2)\n    price = models.DecimalField(max_digits=9, decimal_places=2)\n\n    class Meta:\n        verbose_name = _(\"Sales Transaction\")\n        verbose_name_plural = _(\"Sales Transactions\")\n\n    def __str__(self):\n        return f\"{self.number} - {self.date}\"\n\n    def save(\n            self, *args, **kwargs,\n    ):\n        self.value = self.price * self.quantity\n        super().save(*args, **kwargs)\n\n\nclass MonthlySalesSummary(models.Model):\n    \"\"\"Pre-aggregated monthly sales data for demonstrating crosstab_precomputed.\"\"\"\n\n    product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name=_(\"Product\"))\n    month = models.DateField(verbose_name=_(\"Month\"))\n    total_sales = models.DecimalField(max_digits=12, decimal_places=2, verbose_name=_(\"Total Sales\"))\n    total_quantity = models.DecimalField(max_digits=12, decimal_places=2, verbose_name=_(\"Total Quantity\"))\n\n    class Meta:\n        verbose_name = _(\"Monthly Sales Summary\")\n        verbose_name_plural = _(\"Monthly Sales Summaries\")\n        unique_together = (\"product\", \"month\")\n\n    def __str__(self):\n        return f\"{self.product} - {self.month}\"\n"
  },
  {
    "path": "demo_proj/demo_app/reports.py",
    "content": "import datetime\n\nfrom django.db.models import Sum, Q\nfrom django.http import HttpResponse\nfrom django.utils.translation import gettext_lazy as _\n\nfrom slick_reporting.fields import ComputationField\nfrom slick_reporting.views import ListReportView\nfrom slick_reporting.views import ReportView, Chart\nfrom .forms import TotalSalesFilterForm\nfrom .models import SalesTransaction, Product, MonthlySalesSummary\n\n\nclass ProductSales(ReportView):\n    report_title = _(\"Product Sales\")\n    report_description = _(\"Given a typical 'Sale Item' model, this report demonstrate a total of product sold. \"\n                           \"With a bar and a pie charts.\")\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by = \"product\"\n\n    columns = [\n        \"name\",\n        ComputationField.create(\n            method=Sum,\n            field=\"value\",\n            name=\"value__sum\",\n            verbose_name=\"Total sold $\",\n            is_summable=True,\n        ),\n    ]\n\n    # Charts\n    chart_settings = [\n        Chart(\n            \"Total sold $\",\n            Chart.BAR,\n            data_source=[\"value__sum\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass TotalProductSales(ReportView):\n    report_title = _(\"Product Sales Quantity and Value [no auto load]\")\n    report_description = _(\"We compute the report over *two* fields `quantity and `value`.\"\n                           \"Results only load after you press Filter\")\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by = \"product\"\n    columns = [\n        \"name\",\n        ComputationField.create(Sum, \"quantity\", verbose_name=\"Total quantity sold\", is_summable=False),\n        ComputationField.create(Sum, \"value\", name=\"sum__value\", verbose_name=\"Total Value sold $\"),\n    ]\n    auto_load = False  # Require the user to press the filter, useful if the report is resource demanding\n\n    chart_settings = [\n        Chart(\n            \"Total sold $\",\n            Chart.BAR,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n        Chart(\n            \"Total sold $ [PIE]\",\n            Chart.PIE,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass TotalProductSalesByCountry(ReportView):\n    report_title = _(\"Product Sales by Country\")\n    report_description = _(\"Group by using Django's double-underscore traversal (group_by='client__country').\")\n\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by = \"client__country\"  # notice the double underscore\n    columns = [\n        \"client__country\",\n        ComputationField.create(Sum, \"value\", name=\"sum__value\", verbose_name=\"Total Value sold by country $\"),\n    ]\n\n    chart_settings = [\n        Chart(\n            \"Total sold by country $\",\n            Chart.PIE,  # A Pie Chart\n            data_source=[\"sum__value\"],\n            title_source=[\"client__country\"],\n        ),\n    ]\n\n\nclass SumValueComputationField(ComputationField):\n    calculation_method = Sum\n    calculation_field = \"value\"\n    verbose_name = _(\"Sales Value\")\n    name = \"my_value_sum\"\n\n\nclass MonthlyProductSales(ReportView):\n    report_title = _(\"Product Sales Monthly\")\n    report_description = _(\"Breaks product sales into one column per month using a Time Series, \"\n                           \"also demonstrates defining a reusable ComputationField.\")\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by = \"product\"\n    columns = [\"name\", \"sku\"]\n\n    time_series_pattern = \"monthly\"\n    time_series_columns = [\n        SumValueComputationField,\n    ]\n\n    chart_settings = [\n        Chart(\n            _(\"Total Sales Monthly\"),\n            Chart.PIE,\n            data_source=[\"my_value_sum\"],\n            title_source=[\"name\"],\n            plot_total=True,\n        ),\n        Chart(\n            _(\"Sales Monthly [Bar]\"),\n            Chart.COLUMN,\n            data_source=[\"my_value_sum\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass ProductSalesPerClientCrosstab(ReportView):\n    report_title = _(\"Product Sales Per Client Crosstab\")\n    report_description = _(\"A crosstab matrix with products as rows and clients as columns. \"\n                           \"The remainder column (crosstab_compute_remainder=True) \"\n                           \"captures sales not tied to a said clients.\")\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by = \"product\"\n    crosstab_field = \"client\"\n\n    crosstab_columns = [\n        SumValueComputationField,\n    ]\n\n    crosstab_compute_remainder = True  # Add a extra column to the report, capturing the value all other clients\n\n    columns = [\n        \"name\",\n        \"sku\",  # a field that exists on the `Product` model\n        \"__crosstab__\",\n        SumValueComputationField,\n    ]\n\n\nclass ProductSalesPerCountryCrosstab(ReportView):\n    report_title = _(\"Product Sales Per Country Crosstab\")\n    report_description = _(\"Demonstrate a crosstab/pivot on pre-set IDs (US, KW, EG, DE). \"\n                           \"The remainder column collects sales from all other countries.\")\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by = \"product\"\n    crosstab_field = \"client__country\"\n    crosstab_columns = [\n        SumValueComputationField,\n    ]\n\n    crosstab_ids = [\"US\", \"KW\", \"EG\", \"DE\"]\n    crosstab_compute_remainder = True\n\n    columns = [\n        \"name\",\n        \"sku\",\n        \"__crosstab__\",\n        SumValueComputationField,\n    ]\n\n\nclass LastTenSales(ListReportView):\n    report_model = SalesTransaction\n    report_title = \"Last 10 sales\"\n    report_description = (\"A list view (no aggregation) showing the ten most recent individual sale records.\"\n                          \"Uses ListReportView for row-level data instead of grouped summaries.\")\n    date_field = \"date\"\n    filters = [\"product\", \"client\", \"date\"]\n    columns = [\n        \"product__name\",\n        \"client__name\",\n        \"date\",\n        \"quantity\",\n        \"price\",\n        \"value\",\n    ]\n    default_order_by = \"-date\"\n    limit_records = 10\n\n\nclass TotalProductSalesWithCustomForm(TotalProductSales):\n    report_title = _(\"Total Product Sales with Custom Form\")\n    report_description = _(\"Demonstrates a custom Form (form_class) that adds a product-size filter \"\n                           \"alongside the standard date range filters.\")\n    form_class = TotalSalesFilterForm\n    columns = [\n        \"name\",\n        \"size\",\n        ComputationField.create(Sum, \"quantity\", verbose_name=\"Total quantity sold\", is_summable=False),\n        ComputationField.create(Sum, \"value\", name=\"sum__value\", verbose_name=\"Total Value sold $\"),\n    ]\n\n\nclass GroupByReport(ReportView):\n    report_model = SalesTransaction\n    report_title = _(\"Group By Report\")\n    report_description = _(\"Groups sales by product with no date_field set. \"\n                           \"Shows that it's optional — omitting it gives all-time totals \"\n                           \"regardless of the date pickers.\")\n    # date_field = \"date\"\n    group_by = \"product\"\n\n    columns = [\n        \"name\",\n        ComputationField.create(\n            method=Sum,\n            field=\"value\",\n            name=\"value__sum\",\n            verbose_name=\"Total sold $\",\n            is_summable=True,\n        ),\n    ]\n\n    # Charts\n    chart_settings = [\n        Chart(\n            \"Total sold $\",\n            Chart.BAR,\n            data_source=[\"value__sum\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass GroupByTraversingFieldReport(GroupByReport):\n    report_title = _(\"Group By Traversing Field\")\n    report_description = _(\n        \"Groups by a related model field using Django's double-underscore traversal (group_by='product__product_category'). No date filtering is applied.\")\n    group_by = \"product__product_category\"\n\n\nclass GroupByCustomQueryset(ReportView):\n    report_model = SalesTransaction\n    report_title = _(\"Group By Custom Queryset\")\n    report_description = _(\"Replaces automatic group-by with three crafted querysets (big, small, medium).\"\n                           \"`format_row()` substitutes readable labels for the row's numeric index.\")\n    date_field = \"date\"\n\n    group_by_custom_querysets = [\n        SalesTransaction.objects.filter(product__size__in=[\"big\", \"extra_big\"]),\n        SalesTransaction.objects.filter(product__size__in=[\"small\", \"extra_small\"]),\n        SalesTransaction.objects.filter(product__size=\"medium\"),\n    ]\n    group_by_custom_querysets_column_verbose_name = _(\"Product Size\")\n\n    columns = [\n        \"__index__\",\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Sold $\"), name=\"value\"),\n    ]\n\n    chart_settings = [\n        Chart(\n            title=\"Total sold By Size $\",\n            type=Chart.BAR,\n            data_source=[\"value\"],\n            title_source=[\"__index__\"],\n        ),\n    ]\n\n    def format_row(self, row_obj):\n        # Put the verbose names we need instead of the integer index\n        index = row_obj[\"__index__\"]\n        if index == 0:\n            row_obj[\"__index__\"] = \"Big\"\n        elif index == 1:\n            row_obj[\"__index__\"] = \"Small\"\n        elif index == 2:\n            row_obj[\"__index__\"] = \"Medium\"\n        return row_obj\n\n\nclass NoGroupByReport(ReportView):\n    report_model = SalesTransaction\n    report_title = _(\"No-Group-By Report\")\n    report_description = _(\"Produces a single summary row for the whole dataset with no grouping.\"\n                           \"Useful when you need a grand total over a date range rather than a breakdown.\")\n    date_field = \"date\"\n    group_by = \"\"\n\n    columns = [\n        ComputationField.create(\n            method=Sum,\n            field=\"value\",\n            name=\"value__sum\",\n            verbose_name=\"Total sold $\",\n            is_summable=True,\n        ),\n    ]\n\n\nclass TimeSeriesReport(ReportView):\n    report_title = _(\"Time Series Report\")\n    report_description = _(\n        \"Groups clients as rows and generates one column per month across the date range. The time_series_columns list defines what is computed for each period.\")\n    report_model = SalesTransaction\n    group_by = \"client\"\n    date_field = \"date\"\n\n    # options are : \"daily\", \"weekly\", \"bi-weekly\", \"monthly\", \"quarterly\", \"semiannually\", \"annually\" and \"custom\"\n    time_series_pattern = \"monthly\"\n\n    # These columns will be calculated for each period in the time series.\n    time_series_columns = [\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Sales For \")),\n    ]\n\n    columns = [\n        \"name\",\n        # placeholder for the generated time series columns\n        \"__time_series__\",\n        # This is the same as the time_series_columns, but this one will be on the whole set\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Sales\")),\n    ]\n\n    chart_settings = [\n        Chart(\n            \"Client Sales\",\n            Chart.BAR,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n        Chart(\n            \"Total Sales [Pie]\",\n            Chart.PIE,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            plot_total=True,\n        ),\n        Chart(\n            \"Total Sales [Area chart]\",\n            Chart.AREA,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass TimeSeriesReportWithSelector(TimeSeriesReport):\n    report_title = _(\"Time Series Report With Pattern Selector\")\n    report_description = _(\"Adds a pattern selector (daily / weekly / bi-weekly / monthly) to the filter form.\"\n                           \"users can switch time granularity without changing report code.\")\n    time_series_selector = True\n    time_series_selector_choices = (\n        (\"daily\", _(\"Daily\")),\n        (\"weekly\", _(\"Weekly\")),\n        (\"bi-weekly\", _(\"Bi-Weekly\")),\n        (\"monthly\", _(\"Monthly\")),\n    )\n    time_series_selector_default = \"bi-weekly\"\n\n    # The label for the time series selector\n    time_series_selector_label = _(\"Period Pattern\")\n\n    # Allow the user to select an empty time series, in which case no time series will be applied to the report.\n    time_series_selector_allow_empty = True\n\n\ndef get_current_year():\n    return datetime.datetime.now().year\n\n\nclass TimeSeriesReportWithCustomDates(TimeSeriesReport):\n    report_title = _(\"Time Series Report With Custom Dates\")\n    report_description = _(\"Demonstrates a 'custom' time_series_pattern\"\n                           \"here: first 10 days of Jan, Feb and Mar. \"\n                           \"Useful for non-standard or irregular periods.\")\n    time_series_pattern = \"custom\"\n    time_series_custom_dates = (\n        (datetime.datetime(get_current_year(), 1, 1), datetime.datetime(get_current_year(), 1, 10)),\n        (datetime.datetime(get_current_year(), 2, 1), datetime.datetime(get_current_year(), 2, 10)),\n        (datetime.datetime(get_current_year(), 3, 1), datetime.datetime(get_current_year(), 3, 10)),\n    )\n\n\nclass TimeSeriesReportWithCustomGroupByQueryset(ReportView):\n    report_title = _(\"Time Series Report\")\n    report_description = _(\"Combines custom querysets (US clients vs. RS+DE clients) with time series. \"\n                           \"Each queryset becomes a named row in the resulting matrix.\")\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by_custom_querysets = (\n        SalesTransaction.objects.filter(client__country=\"US\"),\n        SalesTransaction.objects.filter(client__country__in=[\"RS\", \"DE\"]),\n    )\n\n    time_series_pattern = \"monthly\"\n    time_series_columns = [\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Sales For \")),\n    ]\n\n    columns = [\n        \"__index__\",\n        # placeholder for the generated time series columns\n        \"__time_series__\",\n        # This is the same as the time_series_columns, but this one will be on the whole set\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Sales\")),\n    ]\n\n    chart_settings = [\n        Chart(\n            \"Client Sales\",\n            Chart.BAR,\n            data_source=[\"sum__value\"],\n            title_source=[\"__index__\"],\n        ),\n        Chart(\n            \"Total Sales [Pie]\",\n            Chart.PIE,\n            data_source=[\"sum__value\"],\n            title_source=[\"__index__\"],\n            plot_total=True,\n        ),\n        Chart(\n            \"Total Sales [Area chart]\",\n            Chart.AREA,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass SumOfFieldValue(ComputationField):\n    # A custom computation Field with custom verbose names\n    # Similar to `ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Sales\"))`\n\n    calculation_method = Sum\n    calculation_field = \"value\"\n    name = \"sum_of_value\"\n\n    @classmethod\n    def get_time_series_field_verbose_name(cls, date_period, index, dates, pattern):\n        # date_period: is a tuple (start_date, end_date)\n        # index is the  index of the current pattern in the patterns on the report\n        # dates: the whole dates we have on the reports\n        # pattern it's the pattern name, ex: monthly, daily, custom\n        return f\"First 10 days sales {date_period[0].month}-{date_period[0].year}\"\n\n\nclass TimeSeriesReportWithCustomDatesAndCustomTitle(TimeSeriesReportWithCustomDates):\n    report_title = _(\"Time Series Report With Custom Dates and custom Title\")\n    report_description = _(\n        \"Extends custom date ranges with get_time_series_field_verbose_name() \"\n        \"to give each period column a human-readable heading like 'First 10 days sales 1-2024'.\")\n\n    time_series_columns = [\n        SumOfFieldValue,  # Use our newly created ComputationField with the custom time series verbose name\n    ]\n\n    chart_settings = [\n        Chart(\n            \"Client Sales\",\n            Chart.BAR,\n            data_source=[\"sum_of_value\"],  # Note:  This is the name of our `TotalSalesField` computation field\n            title_source=[\"name\"],\n        ),\n        Chart(\n            \"Total Sales [Pie]\",\n            Chart.PIE,\n            data_source=[\"sum_of_value\"],\n            title_source=[\"name\"],\n            plot_total=True,\n        ),\n    ]\n\n\nclass TimeSeriesWithoutGroupBy(ReportView):\n    report_title = _(\"Time Series without a group by\")\n    report_description = _(\"A time series with no group_by: \"\n                           \"the entire dataset becomes one summary row split into pattern (here monthly) columns.\")\n    report_model = SalesTransaction\n    time_series_pattern = \"monthly\"\n    date_field = \"date\"\n    time_series_columns = [\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Sales For \")),\n    ]\n\n    columns = [\n        \"__time_series__\",\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Sales\")),\n    ]\n\n    chart_settings = [\n        Chart(\n            \"Total Sales [Bar]\",\n            Chart.BAR,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n        Chart(\n            \"Total Sales [Pie]\",\n            Chart.PIE,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass CrosstabReport(ReportView):\n    report_title = _(\"Cross tab Report\")\n    report_description = _(\"A basic crosstab: clients as rows, products as columns. \"\n                           \"Each cell holds the sales value for that client–product combination, \"\n                           \"with a total column on the right.\")\n    report_model = SalesTransaction\n    group_by = \"client\"\n    # date_field = \"date\"\n\n    columns = [\n        \"name\",\n        # You can customize where the crosstab columns are displayed in relation to the other columns\n        \"__crosstab__\",\n\n        # This is the same as the calculation in the crosstab,\n        # but this one will be on the whole set. IE total value.\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Value\")),\n    ]\n\n    crosstab_field = \"product\"\n    crosstab_columns = [\n        ComputationField.create(Sum, \"value\", verbose_name=_(\"Value\")),\n    ]\n\n\nclass CrosstabWithTraversingField(CrosstabReport):\n    report_title = _(\"Cross tab Report With Traversing Field\")\n    report_description = _(\"Uses a traversed field (product__size) as the crosstab axis.\")\n    crosstab_field = \"product__size\"\n\n\nclass CrosstabWithIds(CrosstabReport):\n    report_title = _(\"Cross tab Report With Pre-set Ids\")\n    report_description = _(\"Pre-sets the crosstab column IDs to the first and last product via get_crosstab_ids(). \"\n                           \"Useful when you want a known set of columns resolved at request time.\")\n\n    def get_crosstab_ids(self):\n        return [Product.objects.first().pk, Product.objects.last().pk]\n\n\nclass CrosstabWithIdsCustomFilter(CrosstabReport):\n    report_title = _(\"Crosstab with Custom Filters\")\n    report_description = _(\"Replaces per-ID filters with two arbitrary Q-object filters (big vs 'not' big).\"\n                           \"Demonstrates flexibility in breaking down crosstab columns \")\n    crosstab_ids_custom_filters = [\n        (~Q(product__size__in=[\"extra_big\", \"big\"]), dict()),\n        (None, dict(product__size__in=[\"extra_big\", \"big\"])),\n    ]\n    # Note:\n    # if crosstab_ids_custom_filters is set, these settings has NO EFFECT\n    # crosstab_field = \"client\"\n    # crosstab_ids = [1, 2]\n    # crosstab_compute_remainder = True\n\n\nclass CustomCrossTabTotalField(ComputationField):\n    calculation_field = \"value\"\n    calculation_method = Sum\n    verbose_name = _(\"Sales for\")\n    name = \"sum__value\"\n\n    @classmethod\n    def get_crosstab_field_verbose_name(cls, model, id):\n        if id == \"----\":  # 4 dashes: the remainder column\n            return _(\"Rest of Products\")\n\n        name = Product.objects.get(pk=id).name\n        return f\"{cls.verbose_name} {name}\"\n\n\nclass CrossTabReportWithCustomVerboseName(CrosstabReport):\n    report_title = _(\"Crosstab with customized verbose name\")\n    report_description = _(\"Demonstrates how to customize the verbose name\"\n                           \"Here, in each column header, we show 'Sales for Widget A' instead of the raw PK .\")\n    crosstab_columns = [CustomCrossTabTotalField]\n\n\nclass CustomCrossTabTotalPerSize(CustomCrossTabTotalField):\n    @classmethod\n    def get_crosstab_field_verbose_name(cls, model, id):\n        if id == 0:\n            return f\"{cls.verbose_name} Big and Extra Big\"\n        return f\"{cls.verbose_name} all other sizes\"\n\n    @classmethod\n    def get_time_series_field_verbose_name(cls, date_period, index, dates, pattern):\n        return super().get_time_series_field_verbose_name(date_period, index, dates, pattern)\n\n\nclass CrossTabReportWithCustomVerboseNameCustomFilter(CrosstabWithIdsCustomFilter):\n    report_title = _(\"Crosstab customized verbose name with custom filter\")\n    report_description = _(\"Combines Q-object filters with custom verbose names, \"\n                           \"labelling columns 'Big and Extra Big' and 'All other sizes'.\")\n\n    crosstab_columns = [CustomCrossTabTotalPerSize]\n\n\nclass CrossTabWithTimeSeries(CrossTabReportWithCustomVerboseNameCustomFilter):\n    report_title = _(\"Crosstab with time series\")\n    report_description = _(\"Layers a monthly time series on top of the crosstab, producing columns for each filter × period combination. \"\n                           \"Demonstrates the most complex report configuration available.\")\n    date_field = \"date\"\n    time_series_pattern = \"monthly\"\n    crosstab_columns = [CustomCrossTabTotalPerSize]\n    columns = [\"name\", \"__time_series__\"]\n\n\nclass ChartJSExample(TimeSeriesReport):\n    report_title = _(\"ChartJS Examples \")\n    report_description = _(\"The same time-series data visualised with Chart.js. \"\n                           \"Switching chart engines requires only setting\"\n                           \"chart_engine='chartsjs' on the report class.\")\n\n    chart_engine = \"chartsjs\"\n    chart_settings = [\n        Chart(\n            \"Client Sales\",\n            Chart.BAR,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n        Chart(\n            \"Total Sales [Pie]\",\n            Chart.PIE,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            plot_total=True,\n        ),\n        Chart(\n            \"Total Sales [Line total]\",\n            Chart.LINE,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            plot_total=True,\n        ),\n    ]\n\n\nclass HighChartExample(TimeSeriesReport):\n    chart_engine = \"highcharts\"\n    report_title = _(\"Highcharts Examples \")\n    report_description = _(\"Renders the same time-series data with all supported Highcharts chart types.\"\n                           \": column, bar, line, area, pie — including stacked and plot-total variants.\")\n\n    chart_settings = [\n        Chart(\"Columns\", Chart.COLUMN, data_source=[\"sum__value\"], title_source=[\"name\"]),\n        Chart(\n            \"Stacking Columns\",\n            Chart.COLUMN,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            stacking=True,\n        ),\n        Chart(\n            \"Totals Column\",\n            Chart.COLUMN,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            plot_total=True,\n        ),\n        Chart(\n            \"Total Stacking Column\",\n            Chart.COLUMN,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            plot_total=True,\n            stacking=True,\n        ),\n        Chart(\n            \"Bar\",\n            Chart.BAR,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n        Chart(\n            \"Totals Bar\", Chart.BAR, data_source=[\"sum__value\"], title_source=[\"name\"], plot_total=True\n        ),\n        Chart(\n            \"Line Chart\",\n            Chart.LINE,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            # plot_total=True,\n        ),\n        Chart(\n            \"Total Line chart\",\n            Chart.LINE,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            plot_total=True,\n        ),\n        Chart(\n            \"Pie: Total Sales\",\n            Chart.PIE,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n            plot_total=True,\n        ),\n        Chart(\n            \"Area: Client Sales\",\n            Chart.AREA,\n            data_source=[\"sum__value\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass ProductSalesApexChart(ReportView):\n    report_title = _(\"Product Sales Apex Charts\")\n    report_description = _(\n        \"Demonstrates the ApexCharts engine with a custom template and \"\n        \"a custom JS entry point (displayChartCustomEntryPoint) for fully bespoke chart initialisation.\")\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by = \"product\"\n    chart_engine = \"apexcharts\"\n    template_name = \"demo/apex_report.html\"\n\n    columns = [\n        \"name\",\n        ComputationField.create(\n            method=Sum,\n            field=\"value\",\n            name=\"value__sum\",\n            verbose_name=\"Total sold $\",\n            is_summable=True,\n        ),\n    ]\n\n    chart_settings = [\n        Chart(\n            \"Total sold $\",\n            type=\"pie\",\n            data_source=[\"value__sum\"],\n            title_source=[\"name\"],\n        ),\n        Chart(\n            \"Total sold $\",\n            type=\"bar\",\n            data_source=[\"value__sum\"],\n            title_source=[\"name\"],\n        ),\n        Chart(\n            \"A custom Entry Point $\",\n            type=\"bar\",\n            data_source=[\"value__sum\"],\n            title_source=[\"name\"],\n            entryPoint=\"displayChartCustomEntryPoint\", # a custom entry point to control the chart\n        ),\n    ]\n\n\nclass CustomExportReport(GroupByReport):\n    report_title = _(\"Custom Export Report\")\n    report_description = _(\"Demonstrates adding custom action (here export_pdf) action alongside the built-ins\"\n                           \"Also shows how to customize buttons label and css class\")\n    export_actions = [\"export_pdf\"]\n\n    def export_pdf(self, report_data):\n        return HttpResponse(f\"Dummy PDF Exported \\n {report_data}\")\n\n    export_pdf.title = _(\"Export PDF\") # The label for the export action button\n    export_pdf.css_class = \"btn btn-secondary\" # the button classes\n\n    def export_csv(self, report_data):\n        return super().export_csv(report_data)\n\n    export_csv.title = _(\"My Custom CSV export Title\")\n    export_csv.css_class = \"btn btn-primary\"\n\n\nclass ReportWithFormInitial(ReportView):\n    report_title = _(\"Report With Form Initial\")\n    report_description = _(\"Pre-populates the client filter with the first and last client using get_initial(). \"\n                           \"Shows how to set dynamic default filter values based on live data.\")\n    report_model = SalesTransaction\n    date_field = \"date\"\n    group_by = \"product\"\n\n    columns = [\n        \"name\",\n        ComputationField.create(\n            method=Sum,\n            field=\"value\",\n            name=\"value__sum\",\n            verbose_name=\"Total sold $\",\n            is_summable=True,\n        ),\n    ]\n\n    def get_initial(self):\n        from .models import Client\n        initial = super().get_initial()\n        initial[\"client_id\"] = [Client.objects.first().pk, Client.objects.last().pk]\n        return initial\n\n\nclass PreComputedMonthlySales(ReportView):\n    report_title = _(\"Crosstab Precomputed: Monthly Sales\")\n    report_description = _(\"Uses crosstab_precomputed=True on a model whose rows are already aggregated. \"\n                           \"Columns are the distinct month values discovered at query time — no aggregation needed.\")\n    report_model = MonthlySalesSummary\n    date_field = \"month\"\n    group_by = \"product\"\n    crosstab_field = \"month\"\n    crosstab_precomputed = True # signals that data is already aggregated\n    crosstab_columns = [\"total_sales\", \"total_quantity\"] # These fields are already computed/aggregated in database\n    columns = [\"name\", \"__crosstab__\"]\n\n    chart_settings = [\n        Chart(\n            _(\"Monthly Sales by Product\"),\n            Chart.BAR,\n            data_source=[\"total_sales\"],\n            title_source=[\"name\"],\n        ),\n        Chart(\n            _(\"Monthly Quantity by Product\"),\n            Chart.LINE,\n            data_source=[\"total_quantity\"],\n            title_source=[\"name\"],\n        ),\n    ]\n\n\nclass DynamicModelSalesByCountry(ReportView):\n    report_title = _(\"Raw SQL Table / Dynamic Model Sales by Country\")\n    report_description = _(\"Here we're acting on raw SQL table (table_name='regional_sales_summary'). \"\n                           \"No Django model is involved — the schema is introspected at runtime.\"\n                           \"Demonstrating it with Pre-computed crosstab, but we can use it with any type of reports\")\n    table_name = \"regional_sales_summary\"\n    group_by = \"product_name\"\n    crosstab_field = \"country\"\n    crosstab_columns = [\"total_sales\", \"total_quantity\"]\n    crosstab_precomputed = True\n    columns = [\"product_name\", \"__crosstab__\"]\n\n    chart_settings = [\n        Chart(\n            _(\"Sales by Country\"),\n            Chart.BAR,\n            data_source=[\"total_sales\"],\n            title_source=[\"product_name\"],\n        ),\n        Chart(\n            _(\"Sales by Country [Pie]\"),\n            Chart.PIE,\n            data_source=[\"total_sales\"],\n            title_source=[\"product_name\"],\n        ),\n    ]\n"
  },
  {
    "path": "demo_proj/demo_app/templatetags/__init__.py",
    "content": ""
  },
  {
    "path": "demo_proj/demo_app/templatetags/slick_reporting_demo_tags.py",
    "content": "import inspect\n\nfrom django import template\nfrom django.urls import reverse\nfrom django.utils.html import format_html\nfrom django.utils.safestring import mark_safe\n\nregister = template.Library()\n\n\ndef get_section(section):\n    from ..helpers import TUTORIAL, GROUP_BY, TIME_SERIES, CROSSTAB, PIVOT\n    to_use = []\n\n    if section == \"tutorial\":\n        to_use = TUTORIAL\n    elif section == \"group_by\":\n        to_use = GROUP_BY\n    elif section == \"timeseries\":\n        to_use = TIME_SERIES\n    elif section == \"crosstab\":\n        to_use = CROSSTAB\n    elif section == \"pivot\":\n        to_use = PIVOT\n    return to_use\n\n\n@register.simple_tag(takes_context=True)\ndef get_menu(context, section):\n    request = context['request']\n    to_use = get_section(section)\n    menu = []\n    for link, report in to_use:\n        is_active = \"active\" if f\"/{link}/\" in request.path else \"\"\n\n        menu.append(format_html(\n            '<a class=\"dropdown-item {active}\" href=\"{href}\">{text}</a>', active=is_active,\n            href=reverse(link), text=report.report_title or link)\n        )\n\n    return mark_safe(\"\".join(menu))\n\n\n@register.simple_tag\ndef get_report_source(report):\n    try:\n        return inspect.getsource(report.__class__)\n    except (OSError, TypeError):\n        return \"# Source code not available\"\n\n\n@register.simple_tag\ndef get_report_class_label(report):\n    cls = report.__class__\n    return f\"{cls.__module__}.{cls.__name__}\"\n\n\n@register.simple_tag(takes_context=True)\ndef should_show(context, section):\n    request = context[\"request\"]\n    to_use = get_section(section)\n    for link, report in to_use:\n        if f\"/{link}/\" in request.path:\n            return \"show\"\n    return \"\"\n"
  },
  {
    "path": "demo_proj/demo_app/tests.py",
    "content": "import datetime\n\nfrom django.contrib.auth import get_user_model\nfrom django.db import connection\nfrom django.test import TestCase\nfrom django.utils import timezone\n\nfrom . import helpers\nfrom .models import Client, MonthlySalesSummary, Product, ProductCategory, SalesTransaction\n\nALL_REPORTS = helpers.TUTORIAL + helpers.GROUP_BY + helpers.TIME_SERIES + helpers.CROSSTAB + helpers.PIVOT + helpers.OTHER\n\n\nclass DemoSanityTests(TestCase):\n    @classmethod\n    def setUpTestData(cls):\n        cls.superuser = get_user_model().objects.create_superuser(\"admin\", \"admin@example.com\", \"password\")\n        category = ProductCategory.objects.create(name=\"Electronics\")\n\n        cls.p1 = Product.objects.create(name=\"Widget A\", product_category=category, size=\"medium\")\n        cls.p2 = Product.objects.create(name=\"Widget B\", product_category=category, size=\"big\")\n        cls.p3 = Product.objects.create(name=\"Widget C\", product_category=category, size=\"small\")\n\n        cls.c1 = Client.objects.create(name=\"Client US\", country=\"US\")\n        cls.c2 = Client.objects.create(name=\"Client DE\", country=\"DE\")\n        cls.c3 = Client.objects.create(name=\"Client KW\", country=\"KW\")\n\n        pairs = [\n            (cls.p1, cls.c1), (cls.p2, cls.c2), (cls.p1, cls.c3),\n            (cls.p3, cls.c1), (cls.p2, cls.c2), (cls.p3, cls.c3),\n        ]\n        for i, (product, client) in enumerate(pairs):\n            SalesTransaction.objects.create(\n                number=f\"INV-{i + 1:04d}\",\n                date=timezone.make_aware(datetime.datetime(2024, (i % 12) + 1, 15)),\n                client=client,\n                product=product,\n                quantity=10,\n                price=100,\n            )\n\n        for product in [cls.p1, cls.p2, cls.p3]:\n            for month in range(1, 4):\n                MonthlySalesSummary.objects.create(\n                    product=product,\n                    month=datetime.date(2024, month, 1),\n                    total_sales=1000,\n                    total_quantity=10,\n                )\n\n        with connection.cursor() as cursor:\n            for row in [\n                (\"Widget A\", \"US\", 5000, 50),\n                (\"Widget B\", \"DE\", 3000, 30),\n                (\"Widget C\", \"KW\", 2000, 20),\n            ]:\n                cursor.execute(\n                    \"INSERT INTO regional_sales_summary (product_name, country, total_sales, total_quantity)\"\n                    \" VALUES (%s, %s, %s, %s)\",\n                    row,\n                )\n\n    def setUp(self):\n        self.client.force_login(self.superuser)\n\n    def test_all_pages_load(self):\n        for url in [\"/\", \"/dashboard/\"]:\n            with self.subTest(url=url):\n                self.assertEqual(self.client.get(url).status_code, 200)\n\n        for name, _ in ALL_REPORTS:\n            with self.subTest(name=name):\n                response = self.client.get(f\"/{name}/\")\n                self.assertEqual(response.status_code, 200)\n\n    def test_all_report_data_endpoints(self):\n        for name, _ in ALL_REPORTS:\n            with self.subTest(name=name):\n                response = self.client.get(\n                    f\"/{name}/\",\n                    data={\"start_date\": \"2024-01-01\", \"end_date\": \"2024-12-31\"},\n                    HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n                )\n                self.assertEqual(response.status_code, 200)\n                self.assertIn(\"data\", response.json())\n\n    def test_precomputed_crosstab_fk_group_by_returns_data(self):\n        \"\"\"Regression: precomputed crosstab with FK group_by was returning empty rows.\n\n        The generator was building main_queryset as .values(\"product_id\") so each obj\n        only had {\"product_id\": N}. _get_record_data then looked up obj[\"id\"] which was\n        None, causing every group key to resolve to \"None\" and miss the precomputed dict.\n        Fix: fetch the related model objects (same as non-precomputed FK path).\n        \"\"\"\n        response = self.client.get(\n            \"/precomputed-monthly-sales/\",\n            data={\"start_date\": \"2024-01-01\", \"end_date\": \"2024-12-31\"},\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        data = response.json()[\"data\"]\n\n        self.assertEqual(len(data), 3, \"Expected one row per product\")\n        product_names = {row[\"name\"] for row in data}\n        self.assertEqual(product_names, {\"Widget A\", \"Widget B\", \"Widget C\"})\n\n        for row in data:\n            crosstab_values = [v for k, v in row.items() if k not in (\"name\",) and v != \"\"]\n            self.assertTrue(\n                any(v not in (0, \"0\", None) for v in crosstab_values),\n                f\"All crosstab values are zero/empty for {row['name']} — group key lookup is broken\",\n            )\n"
  },
  {
    "path": "demo_proj/demo_app/views.py",
    "content": "from django.views.generic import TemplateView\n\n\n# Create your views here.\n\nclass HomeView(TemplateView):\n    template_name = \"home.html\"\n\n\nclass Dashboard(TemplateView):\n    template_name = \"dashboard.html\"\n"
  },
  {
    "path": "demo_proj/demo_proj/__init__.py",
    "content": ""
  },
  {
    "path": "demo_proj/demo_proj/asgi.py",
    "content": "\"\"\"\nASGI config for demo_proj project.\n\nIt exposes the ASGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/4.2/howto/deployment/asgi/\n\"\"\"\n\nimport os\n\nfrom django.core.asgi import get_asgi_application\n\nos.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"demo_proj.settings\")\n\napplication = get_asgi_application()\n"
  },
  {
    "path": "demo_proj/demo_proj/settings.py",
    "content": "\"\"\"\nDjango settings for demo_proj project.\n\nGenerated by 'django-admin startproject' using Django 4.2.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/4.2/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/4.2/ref/settings/\n\"\"\"\nimport os\nfrom pathlib import Path\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = \"django-insecure-kb+5wbkzz-dxvmzs%49y07g7zkk9@30w%+u@2@d5x!)daivk&7\"\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\").lower() in (\"true\", \"1\", \"yes\")\n\n_allowed_hosts = os.getenv(\"ALLOWED_HOSTS\", \"\")\nALLOWED_HOSTS = _allowed_hosts.split(\",\") if _allowed_hosts else [\"*\"]\n\n\n\n# Application definition\n\nINSTALLED_APPS = [\n    \"django.contrib.admin\",\n    \"django.contrib.auth\",\n    \"django.contrib.contenttypes\",\n    \"django.contrib.sessions\",\n    \"django.contrib.messages\",\n    \"django.contrib.staticfiles\",\n    \"demo_app\",\n    \"crispy_forms\",\n    \"crispy_bootstrap5\",\n    \"slick_reporting\",\n    # \"slick_reporting.dashboards\",\n]\n\nMIDDLEWARE = [\n    \"django.middleware.security.SecurityMiddleware\",\n    \"django.contrib.sessions.middleware.SessionMiddleware\",\n    \"django.middleware.common.CommonMiddleware\",\n    \"django.middleware.csrf.CsrfViewMiddleware\",\n    \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n    \"django.contrib.messages.middleware.MessageMiddleware\",\n    \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n]\n\nROOT_URLCONF = \"demo_proj.urls\"\n\nTEMPLATES = [\n    {\n        \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n        \"DIRS\": [os.path.join(BASE_DIR, \"templates\")],\n        \"APP_DIRS\": True,\n        \"OPTIONS\": {\n            \"context_processors\": [\n                \"django.template.context_processors.debug\",\n                \"django.template.context_processors.request\",\n                \"django.contrib.auth.context_processors.auth\",\n                \"django.contrib.messages.context_processors.messages\",\n            ],\n        },\n    },\n]\n\nWSGI_APPLICATION = \"demo_proj.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/4.2/ref/settings/#databases\n\nDATABASES = {\n    \"default\": {\n        \"ENGINE\": \"django.db.backends.sqlite3\",\n        \"NAME\": BASE_DIR / \"db.sqlite3\",\n    }\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n    {\n        \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n    },\n    {\n        \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n    },\n    {\n        \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n    },\n    {\n        \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n    },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/4.2/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/4.2/howto/static-files/\n\nSTATIC_URL = \"static/\"\n\n# Default primary key field type\n# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field\n\nDEFAULT_AUTO_FIELD = \"django.db.models.BigAutoField\"\n\nCRISPY_TEMPLATE_PACK = \"bootstrap5\"\nCRISPY_ALLOWED_TEMPLATE_PACKS = \"bootstrap5\"\n\nSLICK_REPORTING_DEFAULT_CHARTS_ENGINE = \"highcharts\"\nSLICK_REPORTING_SETTINGS = {\n    \"CHARTS\": {\n        \"apexcharts\": {\n            \"entryPoint\": \"DisplayApexPieChart\",\n            \"js\": (\"https://cdn.jsdelivr.net/npm/apexcharts\", \"slick_reporting/slick_reporting.chartsjs.js\"),\n            \"css\": {\"all\": (\"https://cdn.jsdelivr.net/npm/apexcharts/dist/apexcharts.min.css\",)},\n        },\n    },\n}\n\nSTATIC_ROOT = os.getenv(\"STATIC_ROOT\", BASE_DIR / \"collected_static\")\nMEDIA_ROOT = os.getenv(\"MEDIA_ROOT\", str(BASE_DIR / \"media\"))"
  },
  {
    "path": "demo_proj/demo_proj/urls.py",
    "content": "\"\"\"\nURL configuration for demo_proj project.\n\nThe `urlpatterns` list routes URLs to views. For more information please see:\n    https://docs.djangoproject.com/en/4.2/topics/http/urls/\nExamples:\nFunction views\n    1. Add an import:  from my_app import views\n    2. Add a URL to urlpatterns:  path('', views.home, name='home')\nClass-based views\n    1. Add an import:  from other_app.views import Home\n    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')\nIncluding another URLconf\n    1. Import the include() function: from django.urls import include, path\n    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))\n\"\"\"\nfrom django.contrib import admin\nfrom django.urls import path\n\nfrom demo_app import views\nfrom demo_app import helpers\n\nurlpatterns = helpers.get_urls_patterns() + [\n    path(\"\", views.HomeView.as_view(), name=\"home\"),\n    path(\"dashboard/\", views.Dashboard.as_view(), name=\"dashboard\"),\n    path(\"admin/\", admin.site.urls),\n]\n"
  },
  {
    "path": "demo_proj/demo_proj/wsgi.py",
    "content": "\"\"\"\nWSGI config for demo_proj project.\n\nIt exposes the WSGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/\n\"\"\"\n\nimport os, sys\n\nfrom django.core.wsgi import get_wsgi_application\n\nos.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"demo_proj.settings_production\")\nBASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), \"../\")\nsys.path.append(os.path.abspath(BASE_DIR))\n\napplication = get_wsgi_application()\n"
  },
  {
    "path": "demo_proj/manage.py",
    "content": "#!/usr/bin/env python\n\"\"\"Django's command-line utility for administrative tasks.\"\"\"\nimport os\nimport sys\n\n\ndef main():\n    \"\"\"Run administrative tasks.\"\"\"\n\n    os.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"demo_proj.settings\")\n    # add slick reporting to path so that it can be imported\n    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n    sys.path.append(os.path.abspath(BASE_DIR))\n    try:\n        from django.core.management import execute_from_command_line\n    except ImportError as exc:\n        raise ImportError(\n            \"Couldn't import Django. Are you sure it's installed and \"\n            \"available on your PYTHONPATH environment variable? Did you \"\n            \"forget to activate a virtual environment?\"\n        ) from exc\n    execute_from_command_line(sys.argv)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "demo_proj/requirements.txt",
    "content": "django>=4.2\npython-dateutil>=2.8.1\nsimplejson\ndjango-crispy-forms\ncrispy-bootstrap5\n"
  },
  {
    "path": "demo_proj/templates/base.html",
    "content": "<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"/>\n    <title>{% block meta_page_title %}{{ report_title }}{% endblock %}</title>\n    <!-- CSS files -->\n\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler.min.css\">\n\n\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler-flags.min.css\">\n    <link rel=\"stylesheet\"\n          href=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler-payments.min.css\">\n    <link rel=\"stylesheet\"\n          href=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler-vendors.min.css\">\n    <link rel=\"stylesheet\"\n          href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css\">\n\n\n    <style>\n        @import url('https://rsms.me/inter/inter.css');\n\n        :root {\n            --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;\n        }\n\n        body {\n            font-feature-settings: \"cv03\", \"cv04\", \"cv11\";\n        }\n\n        @media (min-width: 576px)\n\n        .navbar-expand-sm.navbar-vertical ~ .navbar, .navbar-expand-sm.navbar-vertical ~ .page-wrapper {\n            margin-left: 18rem;\n        }\n\n        @media (min-width: 576px)\n\n        .navbar-vertical.navbar-expand-sm {\n            width: 18rem;\n        }\n\n    </style>\n</head>\n<body class=\" layout-fluid\">\n{#    <script src=\"./dist/js/demo-theme.min.js?1684106062\"></script>#}\n<div class=\"page\">\n    <!-- Sidebar -->\n    <aside class=\"navbar navbar-vertical navbar-expand-sm navbar-dark\">\n        <div class=\"container-fluid\">\n            <button class=\"navbar-toggler\" type=\"button\">\n                <span class=\"navbar-toggler-icon\"></span>\n            </button>\n            <h1 class=\"navbar-brand navbar-brand-autodark\">\n                <a href=\"#\">\n                    {#          <img src=\"https://preview.tabler.io/static/logo-white.svg\" width=\"110\" height=\"32\" alt=\"Tabler\" class=\"navbar-brand-image\">#}\n                    Django Slick Reporting\n                </a>\n            </h1>\n            <div class=\"collapse navbar-collapse\" id=\"sidebar-menu\">\n                {% include \"menu.html\" %}\n            </div>\n        </div>\n    </aside>\n    <div class=\"page-wrapper\">\n        <div class=\"page-header d-print-none\">\n            <div class=\"container-xl\">\n                <div class=\"row g-2 align-items-center\">\n                    <div class=\"col\">\n                        <h2 class=\"page-title\">\n                            {#                            Vertical layout#}\n                            {% block page_title %}\n                            {% endblock %}\n                        </h2>\n                        {% block page_subtitle %}{% endblock %}\n                    </div>\n                </div>\n            </div>\n        </div>\n        <div class=\"page-body\">\n            <div class=\"container-xl\">\n                {% block content %}\n                {% endblock %}\n            </div>\n\n        </div>\n    </div>\n</div>\n\n<!-- Libs JS -->\n<!-- Tabler Core -->\n{#    <script src=\"./dist/js/tabler.min.js?1684106062\" defer></script>#}\n{#    <script src=\"./dist/js/demo.min.js?1684106062\" defer></script>#}\n<script src=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/js/tabler.min.js\"></script>\n<script src=\"https://code.jquery.com/jquery-3.7.0.min.js\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>\n<script>hljs.highlightAll();</script>\n{% block extrajs %}\n{% endblock %}\n</body>\n</html>"
  },
  {
    "path": "demo_proj/templates/dashboard.html",
    "content": "{% extends \"base.html\" %}\n{% load slick_reporting_tags %}\n{% block page_title %} Dashboard {% endblock %}\n{% block meta_page_title %} Dashboard {% endblock %}\n\n\n{% block content %}\n    <div class=\"mb-3 \">\n        <label class=\"form-check form-check-inline\">\n            <input class=\"form-check-input\" type=\"checkbox\" id=\"masonry-toggle\">\n            <span class=\"form-check-label\">Compact layout</span>\n        </label>\n    </div>\n\n    <div id=\"dashboard-container\" class=\"row row-cards\">\n        <div class=\"col-lg-6\">\n            {% get_widget_from_url url_name=\"product-sales\" %}\n        </div>\n        <div class=\"col-lg-6\">\n            {% get_widget_from_url url_name=\"total-product-sales-by-country\" title=\"Widget custom title\" %}\n        </div>\n\n        <div class=\"col-lg-6\">\n            {% get_widget_from_url url_name=\"total-product-sales\" chart_id=1 title=\"Custom default Chart\" %}\n        </div>\n\n        <div class=\"col-lg-6\">\n            {% get_widget_from_url url_name=\"monthly-product-sales\" chart_id=1 display_table=False title=\"No table, Chart Only\" %}\n        </div>\n\n        <div class=\"col-lg-6\">\n            {% get_widget_from_url url_name=\"total-product-sales\" display_chart=False title=\"Table only, no chart\" %}\n        </div>\n\n        <div class=\"col-lg-6\">\n            {% get_widget_from_url url_name=\"total-product-sales\" display_table=False display_chart_selector=False title=\"No Chart Selector, only the assigned one\" %}\n        </div>\n\n        <div class=\"col-lg-6\">\n            {% 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\" %}\n        </div>\n\n    </div>\n\n    <style>\n        .dashboard-masonry {\n            display: flex;\n            flex-wrap: wrap;\n            gap: 1rem;\n            align-items: flex-start;\n        }\n        .dashboard-masonry > * {\n            flex: 0 0 calc(50% - 0.5rem);\n            min-width: 0;\n        }\n        .dashboard-masonry canvas {\n            max-width: 100%;\n        }\n        @media (max-width: 991px) {\n            .dashboard-masonry > * {\n                flex: 0 0 100%;\n            }\n        }\n    </style>\n\n{% endblock %}\n\n{% block extrajs %}\n    {% include \"slick_reporting/js_resources.html\" %}\n    {#  make sure to have the js_resources added to the dashboard page  #}\n\n    {% get_charts_media \"all\" %}\n    {# make sure to add all charts needed media to the dashboard page.  #}\n    {# \"all\" loads all registered chart engines. If a CDN is blocked, it may break the page. #}\n    {# You can also pass the specific chart_settings from a report view to load only what's needed. #}\n\n\n    <script>\n        function custom_js_callback(data, $elem) {\n            // data is the json response from the server\n            // $elem is the jquery object of the element `[data-report-widget]` that the report is attached to.\n\n            console.info(data);\n            console.info($elem);\n            $('#responsePre').text(JSON.stringify(data, null, 4));\n        }\n\n        $('#masonry-toggle').on('change', function () {\n            let $container = $('#dashboard-container');\n            if (this.checked) {\n                $container.removeClass('row row-cards').addClass('dashboard-masonry');\n                $container.children().removeClass('col-lg-6');\n            } else {\n                $container.removeClass('dashboard-masonry').addClass('row row-cards');\n                $container.children().addClass('col-lg-6');\n            }\n        });\n    </script>\n{% endblock %}"
  },
  {
    "path": "demo_proj/templates/demo/apex_report.html",
    "content": "{% extends \"slick_reporting/report.html\" %}\n{% load  slick_reporting_tags %}\n\n{% block content %}\n    {{ block.super }}\n\n{% endblock %}\n\n{% block extrajs %}\n    {{ block.super }}\n\n    <script>\n\n        let chart = null;\n\n        function displayChartCustomEntryPoint(data, $elem, chartOptions) {\n            alert(\"This is a custom entry point for displaying charts. \" +\n                \"Check the console for the sent arguments\")\n            console.log(\"data:\", data);\n            console.log(\"$elem:\", $elem);\n            console.log(\"chartOptions:\", chartOptions);\n        }\n\n        function DisplayApexPieChart(data, $elem, chartOptions) {\n            let legendAndSeries = $.slick_reporting.chartsjs.getGroupByLabelAndSeries(data, chartOptions);\n            let options = {}\n            if (chartOptions.type === \"pie\") {\n                options = {\n                    series: legendAndSeries.series,\n                    chart: {\n                        type: \"pie\",\n                        height: 350\n                    },\n                    labels: legendAndSeries.labels,\n                };\n            } else {\n                options = {\n                    chart: {\n                        type: 'bar'\n                    },\n                    series: [{\n                        name: 'Sales',\n                        data: legendAndSeries.series\n                    }],\n                    xaxis: {\n                        categories: legendAndSeries.labels,\n                    }\n                }\n            }\n\n            try {\n                // destroy old chart, if any\n                chart.destroy();\n            } catch (e) {\n                // do nothing\n            }\n\n            chart = new ApexCharts($elem[0], options);\n            chart.render();\n\n        }\n    </script>\n\n{% endblock %}"
  },
  {
    "path": "demo_proj/templates/home.html",
    "content": "{% extends \"base.html\" %}\n\n{% block content %}\n    <section class=\"jumbotron text-center\">\n        <div class=\"container\">\n            <h1 class=\"jumbotron-heading\">Welcome to Django Slick Reporting</h1>\n            <p class=\"lead text-muted\">The Reporting Engine for Django.</p>\n            \n            <p>\n{#                <a href=\"{% url 'product-sales' %}\" class=\"btn btn-primary my-2\">Start walk through</a>#}\n                <a href=\"https://github.com/ra-systems/django-slick-reporting\" class=\"btn btn-secondary my-2\">Github</a>\n            </p>\n        </div>\n    </section>\n\n    <div class=\"container\">\n        <!-- Example row of columns Create powerful analytics with a simple class syntax.  -->\n        <div class=\"row\">\n            <div class=\"col-md-4\">\n                <h2>Powerful</h2>\n                <p>Effortlessly create Simple, Grouped, Time series and Crosstab reports in a handful of code lines.\n                    You can also create your Custom Calculation easily, which will be integrated with the above reports\n                    types</p>\n                <p>\n{#                    <a class=\"btn btn-secondary\" href=\"https://github.com/ra-systems/slick-reporting-demo\" role=\"button\">This#}\n{#                    site on Github »</a>#}\n                    <a   href=\"{% url 'product-sales' %}\" class=\"btn btn-primary my-2 btn btn-primary\">Begin Walk through</a>\n                </p>\n            </div>\n            <div class=\"col-md-4\">\n                <h2>Chart Wrappers</h2>\n                <p>Slick reporting comes with <a href=\"https://www.highcharts.com/\"> Highcharts </a> and <a\n                        href=\"https://www.chartjs.org/\"> Charts.js </a> wrappers to transform the generated data into\n                    attractive charts in handfule of lines</p>\n                <p>You can check Django Slick Reporting documentation for more in depth information</p>\n                <p><a class=\"btn btn-secondary\" href=\"https://django-slick-reporting.readthedocs.io/\" role=\"button\">Read\n                    the docs »</a></p>\n            </div>\n            <div class=\"col-md-4\">\n                <h2>Open source</h2>\n                <p>Optimized for speed. You can also check this same website and generate more data and test this package on million on records yourself\n\n                    <p><a class=\"btn btn-secondary\" href=\"https://github.com/ra-systems/slick-reporting-demo\" role=\"button\">This\n                    site on Github »</a></p>\n\n{#                    <a class=\"github-button\" href=\"https://github.com/ra-systems/django-slick-reporting\"#}\n{#                       data-color-scheme=\"no-preference: dark; light: dark; dark: dark;\" data-icon=\"octicon-star\"#}\n{#                       data-show-count=\"true\" aria-label=\"Star ra-systems/django-slick-reporting on GitHub\">Star</a>#}\n                </p>\n{#                <p><a class=\"btn btn-secondary\" href=\"#\" role=\"button\">View details »</a></p>#}\n            </div>\n        </div>\n\n        <hr>\n\n    </div>\n\n\n{% endblock %}"
  },
  {
    "path": "demo_proj/templates/menu.html",
    "content": "{% load slick_reporting_demo_tags %}\n\n<ul class=\"navbar-nav pt-lg-3\">\n    <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url \"home\" %}\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/home -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M5 12l-2 0l9 -9l9 9l-2 0\"/><path d=\"M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7\"/><path\n                            d=\"M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Home\n                  </span>\n        </a>\n    </li>\n    <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url \"dashboard\" %}\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/home -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M5 12l-2 0l9 -9l9 9l-2 0\"/><path d=\"M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7\"/><path\n                            d=\"M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Dashboard Example\n                  </span>\n        </a>\n    </li>\n\n    <li class=\"nav-item dropdown \">\n        <a class=\"nav-link dropdown-toggle\" href=\"#navbar-base\" data-bs-toggle=\"dropdown\" data-bs-auto-close=\"false\"\n           role=\"button\" aria-expanded=\"false\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/package -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5\"/><path d=\"M12 12l8 -4.5\"/><path\n                            d=\"M12 12l0 9\"/><path d=\"M12 12l-8 -4.5\"/><path d=\"M16 5.25l-8 4.5\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Tutorial\n                  </span>\n        </a>\n        <div class=\"dropdown-menu {% should_show \"tutorial\" %}\">\n            <div class=\"dropdown-menu-columns\">\n                <div class=\"dropdown-menu-column\">\n                    {% get_menu \"tutorial\" %}\n\n                    {#                    <a class=\"dropdown-item\" href=\"./badges.html\">#}\n                    {#                        Badges#}\n                    {#                        <span class=\"badge badge-sm bg-green-lt text-uppercase ms-auto\">New</span>#}\n                    {#                    </a>#}\n                    {#                    <a class=\"dropdown-item\" href=\"./buttons.html\">#}\n                    {#                        Buttons#}\n                    {#                    </a>#}\n                    {#                    <div class=\"dropend\">#}\n                    {#                        <a class=\"dropdown-item dropdown-toggle\" href=\"#sidebar-cards\" data-bs-toggle=\"dropdown\"#}\n                    {#                           data-bs-auto-close=\"false\" role=\"button\" aria-expanded=\"false\">#}\n                    {#                            Cards#}\n                    {#                            <span class=\"badge badge-sm bg-green-lt text-uppercase ms-auto\">New</span>#}\n                    {#                        </a>#}\n                    {#                        <div class=\"dropdown-menu\">#}\n                    {#                            <a href=\"./cards.html\" class=\"dropdown-item\">#}\n                    {#                                Sample cards#}\n                    {#                            </a>#}\n                    {#                            <a href=\"./card-actions.html\" class=\"dropdown-item\">#}\n                    {#                                Card actions#}\n                    {#                                <span class=\"badge badge-sm bg-green-lt text-uppercase ms-auto\">New</span>#}\n                    {#                            </a>#}\n                    {#                            <a href=\"./cards-masonry.html\" class=\"dropdown-item\">#}\n                    {#                                Cards Masonry#}\n                    {#                            </a>#}\n                    {#                        </div>#}\n                    {#                    </div>#}\n                    {#                    <a class=\"dropdown-item\" href=\"./colors.html\">#}\n                    {#                        Colors#}\n                    {#                    </a>#}\n\n                </div>\n\n            </div>\n        </div>\n    </li>\n\n    <li class=\"nav-item dropdown\">\n        <a class=\"nav-link dropdown-toggle\" href=\"#navbar-base\" data-bs-toggle=\"dropdown\" data-bs-auto-close=\"false\"\n           role=\"button\" aria-expanded=\"false\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/package -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5\"/><path d=\"M12 12l8 -4.5\"/><path\n                            d=\"M12 12l0 9\"/><path d=\"M12 12l-8 -4.5\"/><path d=\"M16 5.25l-8 4.5\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Group By Examples\n                  </span>\n        </a>\n        <div class=\"dropdown-menu {% should_show \"group_by\" %}\">\n            <div class=\"dropdown-menu-columns\">\n                <div class=\"dropdown-menu-column\">\n                    {% get_menu \"group_by\" %}\n\n\n                </div>\n\n            </div>\n        </div>\n    </li>\n\n    <li class=\"nav-item dropdown\">\n        <a class=\"nav-link dropdown-toggle\" href=\"#navbar-base\" data-bs-toggle=\"dropdown\" data-bs-auto-close=\"false\"\n           role=\"button\" aria-expanded=\"false\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/package -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5\"/><path d=\"M12 12l8 -4.5\"/><path\n                            d=\"M12 12l0 9\"/><path d=\"M12 12l-8 -4.5\"/><path d=\"M16 5.25l-8 4.5\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Time Series Examples\n                  </span>\n        </a>\n        <div class=\"dropdown-menu {% should_show \"timeseries\" %}\">\n            <div class=\"dropdown-menu-columns\">\n                <div class=\"dropdown-menu-column\">\n                    {% get_menu \"timeseries\" %}\n\n\n                </div>\n\n            </div>\n        </div>\n    </li>\n\n    <li class=\"nav-item dropdown\">\n        <a class=\"nav-link dropdown-toggle\" href=\"#navbar-base\" data-bs-toggle=\"dropdown\" data-bs-auto-close=\"false\"\n           role=\"button\" aria-expanded=\"false\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/package -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5\"/><path d=\"M12 12l8 -4.5\"/><path\n                            d=\"M12 12l0 9\"/><path d=\"M12 12l-8 -4.5\"/><path d=\"M16 5.25l-8 4.5\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Crosstab Examples\n                  </span>\n        </a>\n        <div class=\"dropdown-menu {% should_show \"crosstab\" %}\">\n            <div class=\"dropdown-menu-columns\">\n                <div class=\"dropdown-menu-column\">\n                    {% get_menu \"crosstab\" %}\n\n\n                </div>\n\n            </div>\n        </div>\n    </li>\n\n    <li class=\"nav-item\">\n        <a class=\"nav-link {% if request.path == \"/precomputed-monthly-sales/\" %}active{% endif %}\"\n           href=\"{% url \"precomputed-monthly-sales\" %}\">\n            <span class=\"nav-link-icon d-md-none d-lg-inline-block\">\n                <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                     stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                     stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                        d=\"M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5\"/><path d=\"M12 12l8 -4.5\"/><path\n                        d=\"M12 12l0 9\"/><path d=\"M12 12l-8 -4.5\"/><path d=\"M16 5.25l-8 4.5\"/></svg>\n            </span>\n            <span class=\"nav-link-title\">Pre Computed</span>\n        </a>\n    </li>\n\n    <li class=\"nav-item\">\n        <a class=\"nav-link {% if request.path == \"/dynamic-model-sales-by-country/\" %}active{% endif %}\"\n           href=\"{% url \"dynamic-model-sales-by-country\" %}\">\n            <span class=\"nav-link-icon d-md-none d-lg-inline-block\">\n                <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                     stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                     stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                        d=\"M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5\"/><path d=\"M12 12l8 -4.5\"/><path\n                        d=\"M12 12l0 9\"/><path d=\"M12 12l-8 -4.5\"/><path d=\"M16 5.25l-8 4.5\"/></svg>\n            </span>\n            <span class=\"nav-link-title\">Raw SQL Table (not a django model)</span>\n        </a>\n    </li>\n\n    <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url \"highcharts-examples\" %}\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/home -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M5 12l-2 0l9 -9l9 9l-2 0\"/><path d=\"M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7\"/><path\n                            d=\"M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    HighCharts Charts Demo\n                  </span>\n        </a>\n    </li>\n\n    <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url \"chartjs-examples\" %}\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/home -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M5 12l-2 0l9 -9l9 9l-2 0\"/><path d=\"M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7\"/><path\n                            d=\"M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Charts.js Charts\n                  </span>\n        </a>\n    </li>\n    <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url \"apexcharts-examples\" %}\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/home -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M5 12l-2 0l9 -9l9 9l-2 0\"/><path d=\"M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7\"/><path\n                            d=\"M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Apex Charts\n                  </span>\n        </a>\n    </li>\n    <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url \"custom-export\" %}\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/home -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M5 12l-2 0l9 -9l9 9l-2 0\"/><path d=\"M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7\"/><path\n                            d=\"M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Custom Export\n                  </span>\n        </a>\n    </li>\n\n    <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url \"form-initial\" %}\">\n                  <span class=\"nav-link-icon d-md-none d-lg-inline-block\"><!-- Download SVG icon from http://tabler-icons.io/i/home -->\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"\n                         stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\"\n                         stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/><path\n                            d=\"M5 12l-2 0l9 -9l9 9l-2 0\"/><path d=\"M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7\"/><path\n                            d=\"M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6\"/></svg>\n                  </span>\n            <span class=\"nav-link-title\">\n                    Form initial\n                  </span>\n        </a>\n    </li>\n</ul>"
  },
  {
    "path": "demo_proj/templates/slick_reporting/base.html",
    "content": "{% extends \"base.html\" %}\n\n{% block meta_page_title %} {{ report_title }}{% endblock %}\n{% block page_title %} {{ report_title }} {% endblock %}\n{% block page_subtitle %}{% if report.report_description %}<p class=\"text-muted mt-1\">{{ report.report_description }}</p>{% endif %}{% endblock %}\n\n{% block extrajs %}\n    {{ block.super }}\n    {% include \"slick_reporting/js_resources.html\" %}\n{% endblock %}\n"
  },
  {
    "path": "demo_proj/templates/slick_reporting/report_form.html",
    "content": "{% load i18n crispy_forms_tags slick_reporting_demo_tags %}\n<form id=\"reportForm\" class=\"card\">\n    <div class=\"card-header\">\n        <h3 class=\"card-title\">{% translate \"Filters\" %}</h3>\n    </div>\n    <div class=\"card-body\">\n        {% if form and crispy_helper %}\n            {% crispy form crispy_helper %}\n        {% else %}\n            {% crispy form %}\n        {% endif %}\n    </div>\n    <div class=\"card-footer d-flex justify-content-between align-items-center\">\n        <button type=\"button\" class=\"btn btn-outline-secondary btn-sm\"\n                data-bs-toggle=\"modal\" data-bs-target=\"#sourceCodeModal\">\n            &lt;/&gt; {% translate \"View Source\" %}\n        </button>\n        <div>\n            <input type=\"submit\" value=\"{% translate \"Filter\" %}\"\n                   class=\"btn btn-primary refreshReport\" data-get-results-button>\n            {% for export_action in report.get_export_actions %}\n                <button class=\"btn {{ export_action.css_class }}\" data-export-btn\n                        data-export-parameter=\"{{ export_action.parameter }}\"\n                        data-form-selector=\"#reportForm\"\n                        {% if export_action.new_window %}data-export-new-window=\"true\"{% endif %}>\n                    {% if export_action.icon %}<i class=\"{{ export_action.icon }}\"></i> {% endif %}\n                    {{ export_action.title }}\n                </button>\n            {% endfor %}\n        </div>\n    </div>\n</form>\n\n{% get_report_source report as source_code %}\n{% get_report_class_label report as class_label %}\n<div class=\"modal modal-blur fade\" id=\"sourceCodeModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n    <div class=\"modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable\" role=\"document\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header\">\n                <h5 class=\"modal-title font-monospace\">{{ class_label }}</h5>\n                <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"{% translate \"Close\" %}\"></button>\n            </div>\n            <div class=\"modal-body p-0\">\n                <pre class=\"mb-0\"><code class=\"language-python\">{{ source_code }}</code></pre>\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "demo_proj/templates/widget_template_with_pre.html",
    "content": "{% extends \"slick_reporting/widget_template.html\" %}\n{% block widget_content %}\n    <div>\n        <pre id=\"responsePre\"></pre>\n    </div>\n{% endblock %}"
  },
  {
    "path": "docs/requirements.txt",
    "content": "-r ../requirements.txt\ncrispy_bootstrap4\nsphinx\nsphinx_rtd_theme==1.3.0\nreadthedocs-sphinx-search==0.3.1"
  },
  {
    "path": "docs/source/concept.rst",
    "content": ".. _structure:\n\nWelcome to Django Slick Reporting documentation!\n==================================================\n\nDjango Slick Reporting a reporting engine allowing you to create and chart different kind of analytics from your model in a breeze.\n\nDemo site\n---------\n\nIf you haven't yet, please check https://django-slick-reporting.com for a quick walk-though with live code examples..\n\n\n\n:ref:`Tutorial <tutorial>`\n--------------------------\n\nThe 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.\n\n\n\n:ref:`Topic Guides <topics>`\n----------------------------\n\nDiscuss each type of report main structures you can create with Django Slick Reporting and their options.\n\n    * :ref:`Group By report <group_by_topic>`: 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.\n    * :ref:`time_series`: A step further, where the calculations are computed for time periods (day, week, month, custom etc).\n    * :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.\n    * :ref:`list_reports`: Similar to a django admin's changelist, it's a direct view of the report model records\n    * And other topics like how to customize the form, and extend the exporting options.\n\n\n:ref:`Reference <reference>`\n----------------------------\n\nDetailed information about main on Django Slick Reporting's main components\n\n    #. :ref:`Settings <settings>`: The settings you can use to customize the behavior of Django Slick Reporting.\n    #. :ref:`Report View <report_view_options>`: A ``FormView`` CBV subclass with reporting capabilities allowing you to create different types of reports in the view.\n       It provide a default :ref:`Filter Form <filter_form>` to filter the report on.\n       It mimics the Generator API interface, so knowing one is enough to work with the other.\n\n    #. :ref:`Generator <report_generator>`: Responsible for generating report and orchestrating and calculating the computation fields values and mapping them to the results.\n       It has an intuitive API that allows you to define the report structure and the computation fields to be calculated.\n\n    #. :ref:`Computation Field <computation_field>`: a calculation unit,like a Sum or a Count of a certain field.\n       Computation field class set how the calculation should be done. ComputationFields can also depend on each other.\n\n    #. 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\n\n\n\n"
  },
  {
    "path": "docs/source/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\nimport django\n\nsys.path.insert(0, os.path.abspath(\"../../\"))\nos.environ[\"DJANGO_SETTINGS_MODULE\"] = \"tests.settings\"\ndjango.setup()\n\n# -- Project information -----------------------------------------------------\n\nproject = \"Django Slick Reporting\"\ncopyright = \"2020, Ramez Ashraf\"\nauthor = \"Ramez Ashraf\"\n\nmaster_doc = \"index\"\n\n# The full version, including alpha/beta/rc tags\nrelease = \"0.6.8\"\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nautosummary_generate = True\nautoclass_content = \"class\"\nextensions = [\n    \"sphinx.ext.viewcode\",\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.autosummary\",\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = []\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = \"sphinx_rtd_theme\"\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = [\"_static\"]\n"
  },
  {
    "path": "docs/source/howto/customize_frontend.rst",
    "content": "Charting and Front End Customization\n=====================================\n\n\n\nThe ajax response structure\n---------------------------\n\nUnderstanding how the response is structured is imperative in order to customize how the report is displayed on the front end\n\nLet's have a look\n\n.. code-block:: python\n\n\n    # Ajax response or `report_results` template context variable.\n    response = {\n        # the report slug, defaults to the class name all lower\n        \"report_slug\": \"\",\n        # a list of objects representing the actual results of the report\n        \"data\": [\n            {\n                \"name\": \"Product 1\",\n                \"quantity__sum\": \"1774\",\n                \"value__sum\": \"8758\",\n                \"field_x\": \"value_x\",\n            },\n            {\n                \"name\": \"Product 2\",\n                \"quantity__sum\": \"1878\",\n                \"value__sum\": \"3000\",\n                \"field_x\": \"value_x\",\n            },\n            # etc .....\n        ],\n        # A list explaining the columns/keys in the data results.\n        # ie: len(response.columns) == len(response.data[i].keys())\n        # It contains needed information about verbose name , if summable and hints about the data type.\n        \"columns\": [\n            {\n                \"name\": \"name\",\n                \"computation_field\": \"\",\n                \"verbose_name\": \"Name\",\n                \"visible\": True,\n                \"type\": \"CharField\",\n                \"is_summable\": False,\n            },\n            {\n                \"name\": \"quantity__sum\",\n                \"computation_field\": \"\",\n                \"verbose_name\": \"Quantities Sold\",\n                \"visible\": True,\n                \"type\": \"number\",\n                \"is_summable\": True,\n            },\n            {\n                \"name\": \"value__sum\",\n                \"computation_field\": \"\",\n                \"verbose_name\": \"Value $\",\n                \"visible\": True,\n                \"type\": \"number\",\n                \"is_summable\": True,\n            },\n        ],\n        # Contains information about the report as whole if it's time series or a a crosstab\n        # And what's the actual and verbose names of the time series or crosstab specific columns.\n        \"metadata\": {\n            \"time_series_pattern\": \"\",\n            \"time_series_column_names\": [],\n            \"time_series_column_verbose_names\": [],\n            \"crosstab_model\": \"\",\n            \"crosstab_column_names\": [],\n            \"crosstab_column_verbose_names\": [],\n        },\n        # A mirror of the set charts_settings on the ReportView\n        # ``ReportView`` populates the id and the `engine_name' if not set\n        \"chart_settings\": [\n            {\n                \"type\": \"pie\",\n                \"engine_name\": \"highcharts\",\n                \"data_source\": [\"quantity__sum\"],\n                \"title_source\": [\"name\"],\n                \"title\": \"Pie Chart (Quantities)\",\n                \"id\": \"pie-0\",\n            },\n            {\n                \"type\": \"bar\",\n                \"engine_name\": \"chartsjs\",\n                \"data_source\": [\"value__sum\"],\n                \"title_source\": [\"name\"],\n                \"title\": \"Column Chart (Values)\",\n                \"id\": \"bar-1\",\n            },\n        ],\n    }\n\n\nThe ajax response structure\n---------------------------\n\nUnderstanding how the response is structured is imperative in order to customize how the report is displayed on the front end\n\nLet's have a look\n\n.. code-block:: python\n\n\n    # Ajax response or `report_results` template context variable.\n    response = {\n        \"report_slug\": \"\",  # the report slug, defaults to the class name all lower\n        \"data\": [],  # a list of objects representing the actual results of the report\n        \"columns\": [],  # A list explaining the columns/keys in the data results.\n        # ie: len(response.columns) == len(response.data[i].keys())\n        # A List of objects. each object contain field needed information like verbose name , if summable and hints about the data type.\n        \"metadata\": {},  # Contains information about the report as whole if it's time series or a a crosstab\n        # And what's the actual and verbose names of the time series or crosstab specific columns.\n        \"chart_settings\": [],  # a list of objects  mirror of the set charts_settings\n    }\n\n\n"
  },
  {
    "path": "docs/source/howto/index.rst",
    "content": ".. _how_to:\n\n=======\nHow To\n=======\nIn this section we will go over some of the frequent tasks you will need to do when using ReportView.\n\n\nCustomize the form\n==================\n\nThe filter form is automatically generated for convenience\nbut you can override it and add your own Form.\n\nThe system expect that the form used with the ``ReportView`` to implement the ``slick_reporting.forms.BaseReportForm`` interface.\nThe 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.\n\n#. get_filters: return the filters to be used in the report in a tuple\n   The first element is a list of Q filters (is any)\n   The second element is a dict of filters to be used in the queryset\n   These filters will be passed to the report_model.objects.filter(*q_filters, **kw_filters)\n\n#. get_start_date: return the start date to be used in the report\n\n#. get_end_date: return the end date to be used in the report\n\n\n\n.. code-block:: python\n\n    # forms.py\n    from slick_reporting.forms import BaseReportForm\n\n\n    class RequestFilterForm(BaseReportForm, forms.Form):\n\n        SECURE_CHOICES = (\n            (\"all\", \"All\"),\n            (\"secure\", \"Secure\"),\n            (\"non-secure\", \"Not Secure\"),\n        )\n\n        start_date = forms.DateField(\n            required=False,\n            label=\"Start Date\",\n            widget=forms.DateInput(attrs={\"type\": \"date\"}),\n        )\n        end_date = forms.DateField(\n            required=False, label=\"End Date\", widget=forms.DateInput(attrs={\"type\": \"date\"})\n        )\n        secure = forms.ChoiceField(\n            choices=SECURE_CHOICES, required=False, label=\"Secure\", initial=\"all\"\n        )\n        method = forms.CharField(required=False, label=\"Method\")\n\n        other_people_only = forms.BooleanField(\n            required=False, label=\"Show requests from other People Only\"\n        )\n\n        def __init__(self, request=None, *args, **kwargs):\n            self.request = request\n            super().__init__(*args, **kwargs)\n            self.fields[\"start_date\"].initial = datetime.date.today()\n            self.fields[\"end_date\"].initial = datetime.date.today()\n\n        def get_filters(self):\n            q_filters = []\n            kw_filters = {}\n\n            if self.cleaned_data[\"secure\"] == \"secure\":\n                kw_filters[\"is_secure\"] = True\n            elif self.cleaned_data[\"secure\"] == \"non-secure\":\n                kw_filters[\"is_secure\"] = False\n            if self.cleaned_data[\"method\"]:\n                kw_filters[\"method\"] = self.cleaned_data[\"method\"]\n            if self.cleaned_data[\"response\"]:\n                kw_filters[\"response\"] = self.cleaned_data[\"response\"]\n            if self.cleaned_data[\"other_people_only\"]:\n                q_filters.append(~Q(user=self.request.user))\n\n            return q_filters, kw_filters\n\n        def get_start_date(self):\n            return self.cleaned_data[\"start_date\"]\n\n        def get_end_date(self):\n            return self.cleaned_data[\"end_date\"]\n\nFor a complete reference of the ``BaseReportForm`` interface, check :ref:`filter_form_customization`\n\n\nUse the report view in our own template\n---------------------------------------\nTo 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\nYou only need to have a ``{% block content %}`` in your base template to be able to use the report template\nand a ``{% block extrajs %}`` block to add the javascript implementation.\n\n\nThe 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.\n\n.. code-block:: html\n\n    {% extends \"base.html\" %}\n    {% load static %}\n\n    {% block content %}\n\n    {% endblock %}\n\n    {% block project_extrajs %}\n        {% include \"slick_reporting/js_resources.html\" %}\n        {% block extrajs %}\n        {% endblock %}\n\n    {% endblock %}\n\n\nWork with tree data & Nested categories\n---------------------------------------\n\n\n\n\n\nChange the report structure in response to User input\n-----------------------------------------------------\n\n\nCreate your own Chart Engine\n-----------------------------\n\nCreate a Custom ComputationField and reuse it\n---------------------------------------------\n\n\n\nAdd a new chart engine\n----------------------\n\n\nAdd an exporting option\n-----------------------\n\n\n\nWork with categorical data\n--------------------------\n\nHow to create a custom ComputationField\n---------------------------------------\n\n\ncreate custom columns\n---------------------\n\n\nformat numbers in the datatable\n\n\ncustom group by\ncustom time series periods\ncustom crosstab reports\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Topics:\n   :titlesonly:\n\n\n   customize_frontend\n\n\n"
  },
  {
    "path": "docs/source/index.rst",
    "content": "Django Slick Reporting\n======================\n\n**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.\n\n* Create group by , crosstab , timeseries, crosstab in timeseries and list reports in handful line with intuitive syntax\n* Highcharts & Charts.js integration ready to use with the shipped in View, easily extendable to use with your own charts.\n* Export to CSV\n* Easily extendable to add your own computation fields,\n\n\nInstallation\n------------\n\nTo install django-slick-reporting with pip\n\n.. code-block:: bash\n\n        pip install django-slick-reporting\n\n\nUsage\n-----\n\n#. Add ``\"slick_reporting\", \"crispy_forms\", \"crispy_bootstrap4\",`` to ``INSTALLED_APPS``.\n#. Add ``CRISPY_TEMPLATE_PACK = \"bootstrap4\"`` to your ``settings.py``\n#. Execute `python manage.py collectstatic` so the JS helpers are collected and served.\n\n\n\nQuickstart\n----------\n\nYou can start by using ``ReportView`` which is a subclass of ``django.views.generic.FormView``\n\n.. code-block:: python\n\n    # in views.py\n    from slick_reporting.views import ReportView, Chart\n    from slick_reporting.fields import ComputationField\n    from .models import MySalesItems\n    from django.db.models import Sum\n\n\n    class ProductSales(ReportView):\n\n        report_model = MySalesItems\n        date_field = \"date_placed\"\n        group_by = \"product\"\n\n        columns = [\n            \"title\",\n            ComputationField.create(\n                method=Sum, field=\"value\", name=\"value__sum\", verbose_name=\"Total sold $\"\n            ),\n        ]\n\n        # Charts\n        chart_settings = [\n            Chart(\n                \"Total sold $\",\n                Chart.BAR,\n                data_source=[\"value__sum\"],\n                title_source=[\"title\"],\n            ),\n        ]\n\n\n    # in urls.py\n    from django.urls import path\n    from .views import ProductSales\n\n    urlpatterns = [\n        path(\"product-sales/\", ProductSales.as_view(), name=\"product-sales\"),\n    ]\n\nDemo site\n----------\n\nhttps://django-slick-reporting.com is a quick walk-though with live code examples\n\n\n\nNext step :ref:`tutorial`\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n   concept\n   tutorial\n   topics/index\n   ref/index\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n\n"
  },
  {
    "path": "docs/source/ref/computation_field.rst",
    "content": ".. _computation_field_ref:\n\nComputationField API\n--------------------\n\n.. autoclass:: slick_reporting.fields.ComputationField\n\n    .. autoattribute:: name\n    .. autoattribute:: calculation_field\n    .. autoattribute:: calculation_method\n    .. autoattribute:: verbose_name\n    .. autoattribute:: requires\n    .. autoattribute:: type\n\n    .. rubric:: Below are some data passed by the `ReportGenerator`, for extra manipulation, you can change them\n\n    .. autoattribute:: report_model\n    .. autoattribute:: group_by\n    .. autoattribute:: plus_side_q\n    .. autoattribute:: minus_side_q\n\n    .. rubric:: You can customize those methods for maximum control where you can do pretty much whatever you want.\n\n    .. automethod:: prepare\n    .. automethod:: resolve\n    .. automethod:: get_dependency_value\n\n\n\n"
  },
  {
    "path": "docs/source/ref/dynamic_model.rst",
    "content": ".. _dynamic_model_ref:\n\n=============\nDynamic Model\n=============\n\n.. module:: slick_reporting.dynamic_model\n\n``get_dynamic_model``\n---------------------\n\n.. function:: get_dynamic_model(table_name, database=\"default\", schema=None)\n\n    Introspect a database table and return a Django model class for it.\n\n    The returned model has ``managed = False`` and is fully compatible with the Django ORM.\n    Results are cached so repeated calls return the same class.\n\n    :param table_name: The database table name to introspect.\n    :type table_name: str\n    :param database: The database alias from ``DATABASES`` setting.\n    :type database: str\n    :param schema: Optional schema name (PostgreSQL). The schema must be in the\n        connection's ``search_path``. When provided, ``db_table`` is set to\n        ``\"schema\".\"table_name\"``.\n    :type schema: str or None\n    :returns: A Django model class mapped to the table.\n    :rtype: type (subclass of ``django.db.models.Model``)\n    :raises ValueError: If the table does not exist.\n\n\n``table_name`` attribute\n------------------------\n\nBoth ``ReportGenerator`` and ``ReportView`` accept a ``table_name`` parameter.\nWhen set (and ``report_model`` is not), ``get_dynamic_model`` is called automatically.\n\n.. code-block:: python\n\n    class MyReport(ReportView):\n        table_name = \"legacy_sales\"\n        date_field = \"sale_date\"\n        group_by = \"region\"\n        columns = [...]\n\n    # Or via ReportGenerator init\n    report = ReportGenerator(table_name=\"legacy_sales\", ...)\n\nSee :ref:`dynamic_model_topic` for full usage guide and examples.\n"
  },
  {
    "path": "docs/source/ref/index.rst",
    "content": ".. _reference:\n\nReference\n===========\n\nBelow are links to the reference documentation for the various components of the Django slick reporting .\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Components:\n\n   settings\n   view_options\n   computation_field\n   report_generator\n   dynamic_model\n\n\n\n"
  },
  {
    "path": "docs/source/ref/report_generator.rst",
    "content": ".. _report_generator:\n\nReport Generator API\n====================\n\nThe main class responsible generating the report and managing the flow\n\n\nReportGenerator\n---------------\n\n.. autoclass:: slick_reporting.generator.ReportGenerator\n\n    .. rubric:: Below are the basic needed attrs\n    .. autoattribute:: report_model\n    .. autoattribute:: queryset\n    .. autoattribute:: date_field\n    .. autoattribute:: columns\n    .. autoattribute:: group_by\n\n    .. rubric:: Below are the needed attrs and methods for time series manipulation\n    .. autoattribute:: time_series_pattern\n    .. autoattribute:: time_series_columns\n    .. automethod:: get_custom_time_series_dates\n    .. automethod:: get_time_series_field_verbose_name\n\n    .. rubric:: Below are the needed attrs and methods for crosstab manipulation\n    .. autoattribute:: crosstab_field\n    .. autoattribute:: crosstab_columns\n    .. autoattribute:: crosstab_ids\n    .. autoattribute:: crosstab_compute_remainder\n    .. automethod:: get_crosstab_field_verbose_name\n\n    .. rubric:: Below are the magical attrs\n    .. autoattribute:: limit_records\n    .. autoattribute:: swap_sign\n    .. autoattribute:: field_registry_class\n\n\n\n\n\n\n"
  },
  {
    "path": "docs/source/ref/settings.rst",
    "content": ".. _settings:\n\n\nSettings\n========\n\n.. note::\n\n        Settings are changed in version 1.1.1 to being a dictionary instead of individual variables.\n        Variables will continue to work till next major release.\n\n\nBelow are the default settings for django-slick-reporting. You can override them in your settings file.\n\n.. code-block:: python\n\n    SLICK_REPORTING_SETTINGS = {\n        \"JQUERY_URL\": \"https://code.jquery.com/jquery-3.7.0.min.js\",\n        \"DEFAULT_START_DATE_TIME\": datetime(\n            datetime.now().year, 1, 1, 0, 0, 0, tzinfo=timezone.utc\n        ),  # Default: 1st Jan of current year\n        \"DEFAULT_END_DATE_TIME\": datetime.datetime.today(),  # Default to today\n        \"DEFAULT_CHARTS_ENGINE\": SLICK_REPORTING_DEFAULT_CHARTS_ENGINE,\n        \"MEDIA\": {\n            \"override\": False,  # set it to True to override the media files,\n            # False will append the media files to the existing ones.\n            \"js\": (\n                \"https://cdn.jsdelivr.net/momentjs/latest/moment.min.js\",\n                \"https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js\",\n                \"https://cdn.datatables.net/1.13.4/js/dataTables.bootstrap5.min.js\",\n                \"slick_reporting/slick_reporting.js\",\n                \"slick_reporting/slick_reporting.report_loader.js\",\n                \"slick_reporting/slick_reporting.datatable.js\",\n            ),\n            \"css\": {\n                \"all\": (\n                    \"https://cdn.datatables.net/1.13.4/css/dataTables.bootstrap5.min.css\",\n                )\n            },\n        },\n        \"FONT_AWESOME\": {\n            \"CSS_URL\": \"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css\",\n            \"ICONS\": {\n                \"pie\": \"fas fa-chart-pie\",\n                \"bar\": \"fas fa-chart-bar\",\n                \"line\": \"fas fa-chart-line\",\n                \"area\": \"fas fa-chart-area\",\n                \"column\": \"fas fa-chart-column\",\n            },\n        },\n        \"CHARTS\": {\n            \"highcharts\": \"$.slick_reporting.highcharts.displayChart\",\n            \"chartjs\": \"$.slick_reporting.chartjs.displayChart\",\n        },\n        \"MESSAGES\": {\n            \"total\": _(\"Total\"),\n        },\n    }\n\n* JQUERY_URL:\n\n    Link to the jquery file, You can use set it to False and manage the jQuery addition to your liking\n\n* DEFAULT_START_DATE_TIME\n\n    Default date time that would appear on the filter form in the start date\n\n* DEFAULT_END_DATE_TIME\n\n    Default date time that would appear on the filter form in the end date\n\n* FONT_AWESOME:\n\n    Font awesome is used to display the icon next to the chart title. You can override the following settings:\n\n    1. ``CSS_URL``: URL to the font-awesome css file\n    2. ``ICONS``: Icons used for different chart types.\n\n* CHARTS:\n\n    The entry points for displaying charts on the front end.\n    You can add your own chart engine by adding an entry to this dictionary.\n\n* MESSAGES:\n\n   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.\n\n\nOld versions settings:\n\n1. ``SLICK_REPORTING_DEFAULT_START_DATE``: Default: the beginning of the current year\n2. ``SLICK_REPORTING_DEFAULT_END_DATE``: Default: the end of the current  year.\n3. ``SLICK_REPORTING_FORM_MEDIA``: Controls the media files required by the search form.\n   Defaults is:\n\n.. code-block:: python\n\n    SLICK_REPORTING_FORM_MEDIA = {\n        \"css\": {\n            \"all\": (\n                \"https://cdn.datatables.net/v/bs4/dt-1.10.20/datatables.min.css\",\n                \"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css\",\n            )\n        },\n        \"js\": (\n            \"https://code.jquery.com/jquery-3.3.1.slim.min.js\",\n            \"https://cdn.datatables.net/v/bs4/dt-1.10.20/datatables.min.js\",\n            \"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js\",\n            \"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js\",\n            \"https://code.highcharts.com/highcharts.js\",\n        ),\n    }\n\n4. ``SLICK_REPORTING_DEFAULT_CHARTS_ENGINE``: Controls the default chart engine used.\n"
  },
  {
    "path": "docs/source/ref/view_options.rst",
    "content": ".. _report_view_options:\n\n================\nThe Report View\n================\n\n\nBelow is the list of options that can be used in the ReportView class.\n\n\nCore Options\n=============\n\nreport_model\n------------\n\nThe model where the relevant data is stored, in more complex reports,\nit's usually a database view / materialized view.\nYou can customize it at runtime via the ``get_report_model`` hook.\n\n.. code-block:: python\n\n    class MyReportView(ReportView):\n\n        def get_report_model(self):\n            from my_app.models import MyReportModel\n            return MyReportModel.objects.filter(some_field__isnull=False)\n\n\nqueryset\n--------\n\nThe queryset to be used in the report,\nif not specified, it will default to ``report_model._default_manager.all()``\n\ngroup_by\n--------\n\nIf the data in the report_model needs to be grouped by a field.\nIt can be a foreign key, a text field / choice field on the report model or traversing.\n\nExample:\nAssuming we have the following SalesModel\n\n.. code-block:: python\n\n            class SalesModel(models.Model):\n                date = models.DateTimeField()\n                notes = models.TextField(blank=True, null=True)\n                client = models.ForeignKey(\n                    \"client.Client\", on_delete=models.PROTECT, verbose_name=_(\"Client\")\n                )\n                product = models.ForeignKey(\n                    \"product.Product\", on_delete=models.PROTECT, verbose_name=_(\"Product\")\n                )\n                value = models.DecimalField(max_digits=9, decimal_places=2)\n                quantity = models.DecimalField(max_digits=9, decimal_places=2)\n                price = models.DecimalField(max_digits=9, decimal_places=2)\n\nOur ReportView can have the following group_by options:\n\n.. code-block:: python\n\n            from slick_reporting.views import ReportView\n\n            class MyReport(ReportView):\n                report_model = SalesModel\n                group_by = \"product\"  # a field on the model\n                # OR\n                # group_by = 'client__country' a traversing foreign key field\n                # group_by = 'client__gender' a traversing  choice field\n\n\n\n\ncolumns\n-------\nColumns are a list of column names and to make it more flexible,\nyou can pass a tuple of column name and options.\nThe options are only `verbose_name` and `is_summable`.\n\nlike this:\n\n.. code-block:: python\n\n        class MyReport(ReportView):\n                    columns = [\n                        \"id\",\n                        (\"name\", {\"verbose_name\": \"My verbose name\", \"is_summable\": False}),\n                        ]\n\n\n\nA column name can be any of the following:\n\n1. A computation field\n2. A field on the grouped by model\n3. A callable on the view /or the generator\n4. A Special ``__time_series__``, ``__crosstab__``, ``__index__``\n\nLet's take them one by one:\n\n1. A Computation Field.\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAdded as a class or by its name.\nExample:\n\n.. code-block:: python\n\n                from slick_reporting.fields import ComputationField, Sum\n                from slick_reporting.registry import field_registry\n                from slick_reporting.views import ReportView\n\n                @field_registry.register\n                class MyTotalReportField(ComputationField):\n                    name = \"__some_special_name__\"\n\n                class MyReport(ReportView):\n                    columns = [\n                        ComputationField.create(Sum, \"value\", verbose_name=_(\"Value\"), name=\"value\"),\n                        # a computation field created on the fly\n\n                        MyTotalReportField, # Added a a class\n\n                        \"__some_special_name__\", # added by name\n                    ]\n\nFor more information: :ref:`computation_field`\n\n\n2. Fields on the group by model\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nImplying that the group_by is set to a field on the report_model.\n\n.. code-block:: python\n\n        class MyReport(ReportView):\n            report_model = SalesModel\n            group_by = \"client\"\n            columns = [\n                \"name\",  # field that exists on the Client Model\n                \"date_of_birth\",  # field that exists on the Client Model\n                \"agent__name\",  # a traversing field from client model\n                # ...\n            ]\n\n         # If the group_by is traversing then the available columns would be of the model at the end of the traversing\n        class MyOtherReport(ReportView):\n            report_model = MySales\n            group_by = \"client__agent\"\n            columns = [\n                \"name\",\n                \"country\",  # fields that exists on the Agent Model\n                \"contact__email\",  # A traversing field from the Agent model\n            ]\n\n\n.. note::\n\n    If group_by is not set, columns can be only a calculation field. refer to the topic `no_group_by_topic`\n\n\n3. A callable on the view\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe callable should accept the following arguments\n\n        :param obj: a dictionary of the current group_by row\n        :param row: a the current row of the report.\n        :return: the value to be displayed in the report\n\n\n.. code-block:: python\n\n    class Report(ReportView):\n        columns = [ \"field_on_group_by_model\", \"group_by_model__traversing_field\",\n                    \"get_attribute\", ComputationField.create(name=\"example\")]\n\n        def get_attribute(self, obj: dict, row: dict):\n             # obj: a dictionary of the current group_by row\n             # row: a the current row of the report.\n\n            return f\"{obj[\"field_on_group_by_model_2\"]} - {row[\"group_by_model__traversing_field\"]}\"\n\n        get_attribute.verbose_name = \"My awesome title\"\n\n\n4. A Special ``__time_series__``, ``__crosstab__``, ``__index__``\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n``__time_series__``: is used to control the position of the time series columns inside the report.\n\n``__crosstab__``: is used to control the position of the crosstab columns inside the report.\n\n``__index__``: is used to display the index of the report, it's usually used with the ``group_by_custom_querysets`` option.\n\n\n\ndate_field\n----------\n\nThe date field to be used in filtering and computing\n\nstart_date_field_name\n---------------------\nThe name of the start date field, if not specified, it will default to what set in ``date_field``\n\nend_date_field_name\n-------------------\nThe name of the end date field, if not specified, it will default to ``date_field``\n\n\n\nchart_settings\n--------------\nA list of Chart objects representing the charts you want to attach to the report.\n\n        Example:\n\n        .. code-block:: python\n\n            class MyReport(ReportView):\n                report_model = Request\n                # ..\n                chart_settings = [\n                    Chart(\n                        title=\"Browsers\",\n                        type=Chart.PIE, # or just string \"bar\"\n                        title_source=[\"user_agent\"],\n                        data_source=[\"count__id\"],\n                        plot_total=False,\n                    ),\n                    Chart(\n                        \"Browsers Bar Chart\",\n                        Chart.BAR,\n                        title_source=[\"user_agent\"],\n                        data_source=[\"count__id\"],\n                        plot_total=True,\n                    ),\n                ]\n\n\nform_class\n----------\nThe form you need to display to control the results.\nDefault to an automatically generated form containing the start date, end date and all foreign keys on the model.\nFor more information: `filter_form`\n\nexcluded_fields\n-----------------\nFields to be excluded from the automatically generated form\n\n\nauto_load\n--------------\nControl if the report should be loaded automatically on page load or not, default to ``True``\n\n\n``report_title``\n----------------\nThe title of the report to be displayed in the report page.\n\n``report_title_context_key``\n----------------------------\nThe context key to be used to pass the report title to the template, default to ``report_title``.\n\n\n\n``template_name``\n-----------------\n\nThe template to be used to render the report, default to ``slick_reporting/report.html``\nYou can override this to customize the report look and feel.\n\n\n``csv_export_class``\n--------------------\nSet the csv export class to be used to export the report, default to ``ExportToStreamingCSV``\n\n\n``report_generator_class``\n--------------------------\nSet the generator class to be used to generate the report, default to ``ReportGenerator``\n\n``default_order_by``\n--------------------\nA Default order by for the results.\nAs you would expect, for DESC order: default_order_by (or order_by as a parameter) ='-field_name'\n\n.. note::\n\n    Ordering can also be controlled at run time by passing order_by='field_name' as a parameter to the view.\n\n\n``limit_records``\n-----------------\n\nLimit the number of records to be displayed in the report, default to ``None`` (no limit)\n\n``swap_sign``\n--------------\nSwap the sign of the values in the report, default to ``False``\n\n\nDouble Sided Calculations Options\n==================================\n\n.. attribute:: ReportView.with_type\n\n        Set if double sided calculations should be taken into account, default to ``False``\n        Read more about double sided calculations here https://django-erp-framework.readthedocs.io/en/latest/topics/doc_types.html\n\n.. attribute:: ReportView.doc_type_field_name\n\n        Set the doc_type field name to be used in double sided calculations, default to ``doc_type``\n\n.. attribute:: ReportView.doc_type_plus_list\n\n        Set the doc_type plus list to be used in double sided calculations, default to ``None``\n\n.. attribute:: ReportView.doc_type_minus_list\n\n            Set the doc_type minus list to be used in double sided calculations, default to ``None``\n\n\n\nHooks and functions\n====================\n\n.. attribute:: ReportView.get_queryset()\n\n        Override this function to return a custom queryset to be used in the report.\n\n.. attribute:: ReportView.get_report_title()\n\n        Override this function to return a custom report title.\n\n.. attribute:: ReportView.ajax_render_to_response()\n\n            Override this function to return a custom response for ajax requests.\n\n.. attribute:: ReportView.format_row()\n\n        Override this function to return a custom row format.\n\n.. attribute:: ReportView.filter_results(data, for_print=False)\n\n        Hook to Filter results, usable if you want to do actions on the data set based on computed data (like eliminate __balance__ = 0, etc)\n        :param data: the data set , list of dictionaries\n        :param for_print: if the data is being filtered for printing or not\n        :return: the data set after filtering.\n\n.. attribute:: ReportView.get_form_crispy_helper()\n\n        Override this function to return a custom crispy form helper for the report form.\n\n"
  },
  {
    "path": "docs/source/topics/charts.rst",
    "content": "Charts Customization\n====================\n\nCharts Configuration\n---------------------\n\nReportView ``charts_settings`` is a list of objects which each object represent a chart configurations.\nThe chart configurations are:\n\n* title: the Chart title. Defaults to the `report_title`.\n* type: A string. Examples are pie, bar, line, etc ...\n* engine_name: A string, default to the ReportView ``chart_engine`` attribute, then to the ``SLICK_REPORTING_SETTINGS.DEFAULT_CHARTS_ENGINE``.\n* data_source: string, the field name containing the numbers we want to plot.\n* title_source: string, the field name containing labels of the data_source\n* plot_total: if True the chart will plot the total of the columns. Useful with time series and crosstab reports.\n* entryPoint: the javascript entry point to display the chart, the entryPoint function accepts the data, $elem and the chartSettings parameters.\n\nOn front end, for each chart needed we pass the whole response to the relevant chart helper function and it handles the rest.\n\n\n\nCustomizing the entryPoint for a chart\n--------------------------------------\n\nSometimes you want to display the chart differently, in this case, you can just change the entryPoint function.\n\nExample:\n\n.. code-block:: python\n\n    class ProductSalesApexChart(ReportView):\n        # ..\n        template_name = \"product_sales_report.html\"\n        chart_settings = [\n            # ..\n            Chart(\n                \"Total sold $\",\n                type=\"bar\",\n                data_source=[\"value__sum\"],\n                title_source=[\"name\"],\n                entryPoint=\"displayChartCustomEntryPoint\",  # this is the new entryPoint\n            ),\n        ]\n\n\nThen in your template `product_sales_report.html` add the javascript function specified as the new entryPoint.\n\n.. code-block:: html+django\n\n            {% extends \"slick_reporting/report.html\" %}\n            {% load  slick_reporting_tags %}\n            {% block extra_js %}\n                {{ block.super }}\n                <script>\n                    function displayChartCustomEntryPoint(data, $elem, chartSettings) {\n                        // data: is the ajax response coming from server\n                        // $elem: is the jquery element where the chart should be rendered\n                        // chartSettings: is the relevant chart dictionary/object in your ReportView chart_settings\n                        // do your custom logic here\n                    }\n                </script>\n\n            {% endblock %}\n\nAdding a new charting engine\n----------------------------\n\nIn 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.\n\n#. 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``\n\n.. code-block:: python\n\n    SLICK_REPORTING_SETTINGS = {\n        \"CHARTS\": {\n            \"apexcharts\": {\n                \"entryPoint\": \"DisplayApexPieChart\",\n                \"js\": (\n                    \"https://cdn.jsdelivr.net/npm/apexcharts\",\n                    \"js_file_for_apex_chart.js\",  # this file contains the entryPoint function and is responsible\n                    # for compiling the data and rendering the chart\n                ),\n                \"css\": {\n                    \"all\": \"https://cdn.jsdelivr.net/npm/apexcharts/dist/apexcharts.min.css\"\n                },\n            }\n        },\n    }\n\n#. Add the entry point function to the javascript file `js_file_for_apex_chart.js` in this example.\n\nIt can look something like this:\n\n.. code-block:: javascript\n\n    let chart = null;\n    function DisplayApexPieChart(data, $elem, chartOptions) {\n        // Where:\n        // data: is the ajax response coming from server\n        // $elem: is the jquery element where the chart should be rendered\n       // chartOptions: is the relevant chart dictionary/object in your ReportView chart_settings\n\n            let legendAndSeries = $.slick_reporting.chartsjs.getGroupByLabelAndSeries(data, chartOptions);\n            // `getGroupByLabelAndSeries` is a helper function that will return an object with two keys: labels and series\n\n            let options = {}\n            if (chartOptions.type === \"pie\") {\n                options = {\n                    series: legendAndSeries.series,\n                    chart: {\n                        type: \"pie\",\n                        height: 350\n                    },\n                    labels: legendAndSeries.labels,\n                };\n            } else {\n                options = {\n                    chart: {\n                        type: 'bar'\n                    },\n                    series: [{\n                        name: 'Sales',\n                        data: legendAndSeries.series\n                    }],\n                    xaxis: {\n                        categories: legendAndSeries.labels,\n                    }\n                }\n            }\n\n            try {\n                // destroy old chart, if any\n                chart.destroy();\n            } catch (e) {\n                // do nothing\n            }\n\n            chart = new ApexCharts($elem[0], options);\n            chart.render();\n    }\n\n"
  },
  {
    "path": "docs/source/topics/computation_field.rst",
    "content": ".. _computation_field:\n\n\nComputation Field\n=================\n\nComputationFields are the basic unit in a report.they represent a number that is being computed.\n\nComputation Fields can be add to a report as a class, as you saw in other examples , or by name.\n\n\nCreating Computation Fields\n---------------------------\n\nThere are 3 ways you can create a Computation Field\n\n1. Create a subclass of ComputationField and set the needed attributes and use it in the columns attribute of the ReportView\n2. Use the `ComputationField.create()` method and pass the needed attributes and use it in the columns attribute of the ReportView\n3. Use the `report_field_register` decorator to register a ComputationField subclass and use it by its name in the columns attribute of the ReportView\n\n\n\n.. code-block:: python\n\n    from slick_reporting.fields import ComputationField\n    from slick_reporting.decorators import report_field_register\n\n\n    @report_field_register\n    class TotalQTYReportField(ComputationField):\n        name = \"__total_quantity__\"\n        calculation_field = \"quantity\"  # the field we want to compute on\n        calculation_method = Sum  # What method we want, default to Sum\n        verbose_name = _(\"Total quantity\")\n\n\n    class ProductSales(ReportView):\n        report_model = SalesTransaction\n        # ..\n        columns = [\n            # ...\n            \"__total_quantity__\",  # Use the ComputationField by its registered name\n            TotalQTYReportField,  # Use Computation Field as a class\n            ComputationField.create(\n                Sum, \"quantity\", name=\"__total_quantity__\", verbose_name=_(\"Total quantity\")\n            )\n            # Using the ComputationField.create() method\n        ]\n\nWhat happened here is that we:\n\n1. Created a ComputationField subclass and gave it the needed attributes\n2. Register it via ``report_field_register`` so it can be picked up by the framework.\n3. Used it by name inside the columns attribute (or in time_series_columns, or in crosstab_columns)\n4. Note that this is same as using the class directly in the columns , also the same as using `ComputationField.create()`\n\nAnother example, adding and AVG to the field `price`:\n\n.. code-block:: python\n\n    from django.db.models import Avg\n    from slick_reporting.decorators import report_field_register\n\n\n    @report_field_register\n    class TotalQTYReportField(ComputationField):\n        name = \"__avg_price__\"\n        calculation_field = \"price\"\n        calculation_method = Avg\n        verbose_name = _(\"Avg. Price\")\n\n\n    class ProductSales(ReportView):\n        # ..\n        columns = [\n            \"name\",\n            \"__avg_price__\",\n        ]\n\nUsing Value of a Computation Field within a another\n---------------------------------------------------\n\nSometime you want to stack values on top of each other. For example: Net revenue = Gross revenue - Discounts.\n\n.. code-block:: python\n\n    class PercentageToTotalBalance(ComputationField):\n        requires = [BalanceReportField]\n        name = \"__percent_to_total_balance__\"\n        verbose_name = _(\"%\")\n        calculation_method = Sum\n        calculation_field = \"value\"\n\n        prevent_group_by = True\n\n        def resolve(\n            self,\n            prepared_results,\n            required_computation_results: dict,\n            current_pk,\n            current_row=None,\n        ) -> float:\n            result = super().resolve(\n                prepared_results, required_computation_results, current_pk, current_row\n            )\n            return required_computation_results.get(\"__balance__\") / result * 100\n\n\nWe 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.\n\nNote:\n\n1. The ``requires`` attribute is a list of the required fields to be computed before this field.\n2. The values of the ``requires`` fields are available in the ``required_computation_results`` dictionary.\n3. 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.\n\n\nHow it works ?\n--------------\nWhen the `ReportGenerator` is initialized, it generates a list of the needed fields to be displayed and computed.\nEach computation field in the report is given the filters needed and asked to get all the results prepared.\nThen for each record, the ReportGenerator again asks each ComputationField to get the data it has for each record and map it back.\n\n\nCustomizing the Calculation Flow\n--------------------------------\n\nThe results are prepared in 2 main stages\n\n1. 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.\n2. resolve: Where you get the value for each record.\n\n\n\n\n.. code-block:: python\n\n    class MyCustomComputationField(ComputationField):\n        name = \"__custom_field__\"\n\n        def prepare(\n            self,\n            q_filters: list | object = None,\n            kwargs_filters: dict = None,\n            queryset=None,\n            **kwargs\n        ):\n            # do all you calculation here for the whole set if any and return the prepared results\n            pass\n\n        def resolve(\n            self,\n            prepared_results,\n            required_computation_results: dict,\n            current_pk,\n            current_row=None,\n        ) -> float:\n            # does the calculation for each record, return a value\n            pass\n\nBundled Report Fields\n---------------------\nAs this project came form an ERP background, there are some bundled report fields that you can use out of the box.\n\n* __total__ : `Sum` of the field named `value`\n* __total_quantity__ : `Sum` of the field named `quantity`\n* __fb__ : First Balance, Sum of the field `value` on the start date (or period in case of time series)\n* __balance__: Compound Sum of the field `value`. IE: the sum of the field `value` on end date.\n* __credit__: Sum of field Value for the minus_list\n* __debit__: Sum of the field value for the plus list\n* __percent_to_total_balance__: Percent of the field value to the balance\n\nWhat is the difference between total and balance fields ?\n\nTotal: Sum of the value for the period\nBalance: Sum of the value for the period + all the previous periods.\n\nExample: You have a client who buys 10 in Jan., 12 in Feb. and 13 in March:\n\n* `__total__` will return 10 in Jan, 12 in Feb and 13 in March.\n* `__balance__` will return 10 in Jan, 22 in Feb and 35 in March\n\n\n\n"
  },
  {
    "path": "docs/source/topics/crosstab_options.rst",
    "content": ".. _crosstab_reports:\n\nCrosstab Reports\n=================\nUse crosstab reports, also known as matrix reports, to show the relationships between three or more query items.\nCrosstab reports show data in rows and columns with information summarized at the intersection points.\n\n\nGeneral use case\n----------------\nHere is a general use case:\n\n.. code-block:: python\n\n    from django.utils.translation import gettext_lazy as _\n    from django.db.models import Sum\n    from slick_reporting.views import ReportView\n\n\n    class CrosstabReport(ReportView):\n        report_title = _(\"Cross tab Report\")\n        report_model = SalesTransaction\n        group_by = \"client\"\n        date_field = \"date\"\n\n        columns = [\n            \"name\",\n            \"__crosstab__\",\n            # You can customize where the crosstab columns are displayed in relation to the other columns\n            ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Value\")),\n            # This is the same as the calculation in the crosstab,\n            # but this one will be on the whole set. IE total value.\n        ]\n\n        crosstab_field = \"product\"\n        crosstab_columns = [\n            ComputationField.create(Sum, \"value\", verbose_name=_(\"Value\")),\n        ]\n\nCrosstab on a Traversing Field\n------------------------------\nYou can also crosstab on a traversing field. In the example below we extend the previous crosstab report to be on the product sizes\n\n.. code-block:: python\n\n        class CrosstabWithTraversingField(CrosstabReport):\n            crosstab_field = \"product__size\"\n\n\nCustomizing the crosstab ids\n----------------------------\nYou 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\n\n.. code-block:: python\n\n        class CrosstabWithIds(CrosstabReport):\n            def get_crosstab_ids(self):\n                return [Product.objects.first().pk, Product.objects.last().pk]\n\n\nCustomizing the Crosstab Filter\n-------------------------------\n\nFor more fine tuned report, You can customize the crosstab report by supplying a list of tuples to the ``crosstab_ids_custom_filters`` attribute.\nThe 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``.\n\nExample:\n\n.. code-block:: python\n\n        class CrosstabWithIdsCustomFilter(CrosstabReport):\n            crosstab_ids_custom_filters = [\n                (~Q(product__size__in=[\"extra_big\", \"big\"]), dict()),\n                (None, dict(product__size__in=[\"extra_big\", \"big\"])),\n            ]\n            # Note:\n            # if crosstab_ids_custom_filters is set, these settings has NO EFFECT\n            # crosstab_field = \"client\"\n            # crosstab_ids = [1, 2]\n            # crosstab_compute_remainder = True\n\n\n\nCustomizing the verbose name of the crosstab columns\n----------------------------------------------------\nSimilar to what we did in customizing the verbose name of the computation field for the time series,\nHere, 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.\nDefault is that the verbose name will display the id of the crosstab field, and the remainder column will be called \"The remainder\".\n\nLet's see two examples on how we can customize the verbose name.\n\nExample 1: On a \"regular\" crosstab report\n\n.. code-block:: python\n\n        class CustomCrossTabTotalField(ComputationField):\n            calculation_field = \"value\"\n            calculation_method = Sum\n            verbose_name = _(\"Sales for\")\n            name = \"sum__value\"\n\n            @classmethod\n            def get_crosstab_field_verbose_name(cls, model, id):\n                if id == \"----\":  # 4 dashes: the remainder column\n                    return _(\"Rest of Products\")\n\n                name = Product.objects.get(pk=id).name\n                return f\"{cls.verbose_name} {name}\"\n\n\n        class CrossTabReportWithCustomVerboseName(CrosstabReport):\n            crosstab_columns = [CustomCrossTabTotalField]\n\nExample 2: On the ``crosstab_ids_custom_filters`` one\n\n.. code-block:: python\n\n        class CustomCrossTabTotalField2(CustomCrossTabTotalField):\n            @classmethod\n            def get_crosstab_field_verbose_name(cls, model, id):\n                if id == 0:\n                    return f\"{cls.verbose_name} Big and Extra Big\"\n                return f\"{cls.verbose_name} all other sizes\"\n\n\n        class CrossTabReportWithCustomVerboseNameCustomFilter(CrosstabWithIdsCustomFilter):\n            crosstab_columns = [CustomCrossTabTotalField2]\n\n\n\nExample\n-------\n\n.. image:: _static/crosstab.png\n  :width: 800\n  :alt: crosstab\n  :align: center\n\n\n1. The Group By. In this example, it is the product field.\n2. The Crosstab. In this example, it is the client field. crosstab_ids were set to client 1 and client 2\n3. The remainder. In this example, it is the rest of the clients. crosstab_compute_remainder was set to True\n"
  },
  {
    "path": "docs/source/topics/dynamic_model.rst",
    "content": ".. _dynamic_model_topic:\n\n==============\nDynamic Models\n==============\n\nGeneral use case\n----------------\n\nSometimes you need to generate reports from database tables that don't have a corresponding Django model.\nThis is common with:\n\n* Legacy database tables\n* Data warehouse / analytics tables\n* Tables managed by other systems or ETL pipelines\n* Temporary or staging tables\n\nDjango Slick Reporting provides ``get_dynamic_model`` which introspects any database table and creates\na real Django model at runtime. Since the result is a genuine Django model, all report features work\nnatively: group by, time series, crosstab, aggregation, filtering, and charts.\n\n\nBasic usage\n-----------\n\nUse the ``get_dynamic_model`` utility to create a model from an existing table:\n\n.. code-block:: python\n\n    from slick_reporting.dynamic_model import get_dynamic_model\n\n    # Introspect the table and get a Django model class\n    SalesData = get_dynamic_model(\"legacy_sales\")\n\n    # Use it like any Django model\n    SalesData.objects.all()\n    SalesData.objects.filter(region=\"US\").count()\n\nThen use it with ``ReportGenerator`` or ``ReportView`` as usual:\n\n.. code-block:: python\n\n    from slick_reporting.views import ReportView, Chart\n    from slick_reporting.fields import ComputationField\n    from slick_reporting.dynamic_model import get_dynamic_model\n    from django.db.models import Sum\n\n    SalesData = get_dynamic_model(\"legacy_sales\")\n\n    class LegacySalesReport(ReportView):\n        report_model = SalesData\n        date_field = \"sale_date\"\n        group_by = \"product_id\"\n\n        columns = [\n            \"product_id\",\n            ComputationField.create(\n                method=Sum,\n                field=\"amount\",\n                name=\"amount__sum\",\n                verbose_name=\"Total Sales\",\n            ),\n        ]\n\n        chart_settings = [\n            Chart(\n                \"Total Sales\",\n                Chart.BAR,\n                data_source=[\"amount__sum\"],\n                title_source=[\"product_id\"],\n            ),\n        ]\n\n\nUsing ``table_name`` shortcut\n-----------------------------\n\nInstead of calling ``get_dynamic_model`` yourself, you can set ``table_name`` directly on\n``ReportView`` or pass it to ``ReportGenerator``:\n\n.. code-block:: python\n\n    # On a view\n    class LegacySalesReport(ReportView):\n        table_name = \"legacy_sales\"\n        date_field = \"sale_date\"\n        group_by = \"product_id\"\n\n        columns = [\n            \"product_id\",\n            ComputationField.create(\n                method=Sum,\n                field=\"amount\",\n                name=\"amount__sum\",\n                verbose_name=\"Total Sales\",\n            ),\n        ]\n\n\n.. code-block:: python\n\n    # With ReportGenerator directly\n    from slick_reporting.generator import ReportGenerator\n\n    report = ReportGenerator(\n        table_name=\"legacy_sales\",\n        date_field=\"sale_date\",\n        group_by=\"product_id\",\n        columns=[\n            \"product_id\",\n            ComputationField.create(Sum, \"amount\", name=\"amount__sum\", verbose_name=\"Total Sales\"),\n        ],\n        start_date=start,\n        end_date=end,\n    )\n    data = report.get_report_data()\n\nWhen ``table_name`` is set and ``report_model`` is not, the dynamic model is created automatically.\n\n\nUsing a different database\n--------------------------\n\nIf the table is in a non-default database, pass the ``database`` parameter:\n\n.. code-block:: python\n\n    WarehouseData = get_dynamic_model(\"fact_sales\", database=\"warehouse\")\n\nThis uses the database alias defined in your Django ``DATABASES`` setting.\n\n\nWorking with database schemas\n-----------------------------\n\nOn PostgreSQL, tables can live in different schemas (e.g. ``analytics``, ``staging``).\n\n**Prerequisites:** The schema must be accessible via the connection's ``search_path`` so Django's\nintrospection can find the table. You can configure this in your database settings:\n\n.. code-block:: python\n\n    # settings.py\n    DATABASES = {\n        \"default\": {\n            \"ENGINE\": \"django.db.backends.postgresql\",\n            \"NAME\": \"mydb\",\n            \"OPTIONS\": {\n                \"options\": \"-c search_path=analytics,public\",\n            },\n        }\n    }\n\nThen pass the ``schema`` parameter so ORM queries reference the correct schema:\n\n.. code-block:: python\n\n    SalesData = get_dynamic_model(\"sales\", schema=\"analytics\")\n\nThis sets the model's ``db_table`` to ``\"analytics\".\"sales\"`` so all generated SQL uses the\nschema-qualified table name.\n\n**Schema support by database backend:**\n\n+----------------+-------------------------------------------------------------------+\n| Backend        | How schemas work                                                  |\n+================+===================================================================+\n| PostgreSQL     | Use ``schema`` parameter + ensure schema is in ``search_path``    |\n+----------------+-------------------------------------------------------------------+\n| MySQL          | Schemas are databases. Use the ``database`` parameter instead     |\n+----------------+-------------------------------------------------------------------+\n| SQLite         | No schema concept. Just use ``table_name``                        |\n+----------------+-------------------------------------------------------------------+\n\n\nHow it works\n------------\n\n``get_dynamic_model`` performs the following steps:\n\n1. Connects to the database and verifies the table exists.\n2. Uses Django's ``connection.introspection`` to read column names, types, and constraints.\n3. Maps each database column to the appropriate Django field (``IntegerField``, ``CharField``, ``DateField``, etc.).\n4. Creates a Django model class at runtime with ``managed = False`` (no migrations needed).\n5. Registers the model in Django's app registry so the ORM works fully.\n6. Caches the result so subsequent calls for the same table return the same model class.\n\nSince the result is a standard Django model, all ``ReportGenerator`` features work without\nany special handling: ``group_by``, ``time_series_pattern``, ``crosstab_field``, ``ComputationField``,\nform generation, and chart rendering.\n\n\nCaching\n-------\n\nDynamic models are cached in memory. Calling ``get_dynamic_model(\"my_table\")`` multiple times\nreturns the same model class. The cache key includes the database alias and schema, so models\nfor different databases or schemas are cached separately.\n\n\nLimitations\n-----------\n\n* **No foreign key relationships.** Foreign key columns are introspected as plain integer or\n  varchar fields. This means ``group_by`` traversal across relationships (e.g. ``product__category``)\n  is not available. You can group by the FK column directly (e.g. ``product_id``).\n\n* **No automatic form filters for FK fields.** Since there are no FK relations, the auto-generated\n  filter form will only contain date range fields. Supply a custom ``form_class`` if you need\n  additional filters.\n\n* **Schema must be in search_path.** On PostgreSQL, Django's introspection only finds tables\n  visible in the current ``search_path``. The schema must be configured at the connection level.\n\n* **Table structure changes require restart.** Because models are cached, if the table structure\n  changes (columns added/removed), you need to restart the application or clear the cache.\n"
  },
  {
    "path": "docs/source/topics/exporting.rst",
    "content": "Exporting\n=========\n\nExporting to CSV\n-----------------\nTo 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.\n\nThis will call the export_csv on the view class, engaging a `ExportToStreamingCSV`\n\nHaving an `_export` parameter not implemented, ie the view class do not implement ``export_{parameter_name}``,  will be ignored.\n\n\nConfiguring the CSV export option\n---------------------------------\n\nYou can disable the CSV export option by setting the ``csv_export_class`` attribute to ``False`` on the view class.\nand you can override the function and its attributes to customize the button text\n\n.. code-block:: python\n\n    class CustomExportReport(GroupByReport):\n        report_title = _(\"Custom Export Report\")\n\n        def export_csv(self, report_data):\n            return super().export_csv(report_data)\n\n        export_csv.title = _(\"My Custom CSV export Title\")\n        export_csv.css_class = \"btn btn-success\"\n\n\nAdding an export option\n-----------------------\n\nYou can extend the functionality, say you want to export to pdf.\nAdd a ``export_pdf`` method to the view class, accepting the report_data json response and return the response you want.\nThis ``export_pdf` will be called automatically when url parameter contain ``?_export=pdf``\n\n\nExample to add a pdf export option:\n\n.. code-block:: python\n\n    class CustomExportReport(GroupByReport):\n        report_title = _(\"Custom Export Report\")\n        export_actions = [\"export_pdf\"]\n\n        def export_pdf(self, report_data):\n            return HttpResponse(f\"Dummy PDF Exported {report_data}\")\n\n        export_pdf.title = _(\"Export PDF\")\n        export_pdf.icon = \"fa fa-file-pdf-o\"\n        export_pdf.css_class = \"btn btn-primary\"\n\nThe export function should accept the report_data json response and return the response you want.\n\n"
  },
  {
    "path": "docs/source/topics/filter_form.rst",
    "content": ".. _filter_form:\n\nCustomizing Filter Form\n=======================\n\nThe filter form is a form that is used to filter the data to be used in the report.\n\n\nThe generated form\n-------------------\n\nBehind the scene, The view calls ``slick_reporting.form_factory.report_form_factory`` in ``get_form_class`` method.\n``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.\n\nChanging the generated form API is still private, however, you can use your own form easily.\n\nOverriding the Form\n--------------------\n\nThe system expect that the form used with the ``ReportView`` to implement the ``slick_reporting.forms.BaseReportForm`` interface.\n\nThe 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.\n\n\n* ``get_filters``: Mandatory, return a tuple (Q_filters , kwargs filter) to be used in filtering.\n  q_filter: can be none or a series of Django's Q queries\n  kwargs_filter: None or a dictionary of filters\n\n* ``get_start_date``: Mandatory, return the start date of the report.\n\n* ``get_end_date``: Mandatory, return the end date of the report.\n\n* ``get_crispy_helper`` : Optional, return a crispy form helper to be used in rendering the form.\n\nIn case you are working with a crosstab report, you need to implement the following methods:\n\n* ``get_crosstab_compute_remainder``: return a boolean indicating if the remainder should be computed or not.\n\n* ``get_crosstab_ids``: return a list of ids to be used in the crosstab report.\n\n\nAnd in case you are working with a time series report, with a selector on, you need to implement the following method:\n\n* ``get_time_series_pattern``: return a string representing the time series pattern. ie: ``ie: daily, monthly, yearly``\n\nExample a full example of a custom form:\n\n.. code-block:: python\n\n    # forms.py\n    from slick_reporting.forms import BaseReportForm\n\n    # A Normal form , Inheriting from BaseReportForm\n    class RequestLogForm(BaseReportForm, forms.Form):\n\n        SECURE_CHOICES = (\n            (\"all\", \"All\"),\n            (\"secure\", \"Secure\"),\n            (\"non-secure\", \"Not Secure\"),\n        )\n\n        start_date = forms.DateField(\n            required=False,\n            label=\"Start Date\",\n            widget=forms.DateInput(attrs={\"type\": \"date\"}),\n        )\n        end_date = forms.DateField(\n            required=False, label=\"End Date\", widget=forms.DateInput(attrs={\"type\": \"date\"})\n        )\n        secure = forms.ChoiceField(\n            choices=SECURE_CHOICES, required=False, label=\"Secure\", initial=\"all\"\n        )\n        method = forms.CharField(required=False, label=\"Method\")\n        response = forms.ChoiceField(\n            choices=HTTP_STATUS_CODES,\n            required=False,\n            label=\"Response\",\n            initial=\"200\",\n        )\n        other_people_only = forms.BooleanField(\n            required=False, label=\"Show requests from other People Only\"\n        )\n\n        def __init__(self, *args, **kwargs):\n            super(RequestLogForm, self).__init__(*args, **kwargs)\n            # provide initial values and ay needed customization\n            self.fields[\"start_date\"].initial = datetime.date.today()\n            self.fields[\"end_date\"].initial = datetime.date.today()\n\n        def get_filters(self):\n            # return the filters to be used in the report\n            # Note: the use of Q filters and kwargs filters\n            filters = {}\n            q_filters = []\n            if self.cleaned_data[\"secure\"] == \"secure\":\n                filters[\"is_secure\"] = True\n            elif self.cleaned_data[\"secure\"] == \"non-secure\":\n                filters[\"is_secure\"] = False\n            if self.cleaned_data[\"method\"]:\n                filters[\"method\"] = self.cleaned_data[\"method\"]\n            if self.cleaned_data[\"response\"]:\n                filters[\"response\"] = self.cleaned_data[\"response\"]\n            if self.cleaned_data[\"other_people_only\"]:\n                q_filters.append(~Q(user=self.request.user))\n\n            return q_filters, filters\n\n        def get_start_date(self):\n            return self.cleaned_data[\"start_date\"]\n\n        def get_end_date(self):\n            return self.cleaned_data[\"end_date\"]\n\n        # ----\n        # in views.py\n\n        from .forms import RequestLogForm\n\n        class RequestCountByPath(ReportView):\n            form_class = RequestLogForm\n\nYou can view this code snippet in action on the demo project https://django-slick-reporting.com/total-product-sales-with-custom-form/\n"
  },
  {
    "path": "docs/source/topics/group_by_report.rst",
    "content": ".. _group_by_topic:\n\n================\nGroup By Reports\n================\n\nGeneral use case\n----------------\n\nGroup 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.\n\n\nExample:\n\n.. code-block:: python\n\n    class GroupByReport(ReportView):\n        report_model = SalesTransaction\n        report_title = _(\"Group By Report\")\n        date_field = \"date\"\n        group_by = \"product\"\n\n        columns = [\n            \"name\",\n            ComputationField.create(\n                method=Sum,\n                field=\"value\",\n                name=\"value__sum\",\n                verbose_name=\"Total sold $\",\n                is_summable=True,\n            ),\n        ]\n\n        # Charts\n        chart_settings = [\n            Chart(\n                \"Total sold $\",\n                Chart.BAR,\n                data_source=[\"value__sum\"],\n                title_source=[\"name\"],\n            ),\n        ]\n\n\nA Sample group by report would look like this:\n\n.. image:: _static/group_report.png\n  :width: 800\n  :alt: Group Report\n  :align: center\n\nIn 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.\n\nGroup by a traversing field\n---------------------------\n\n``group_by`` value can be a traversing field. If set, the report will be grouped by the last field in the traversing path,\n    and, the columns available will be those of the last model in the traversing path.\n\n\nExample:\n\n.. code-block:: python\n\n    # Inherit from previous report and make another version, keeping the columns and charts\n    class GroupByTraversingFieldReport(GroupByReport):\n\n        report_title = _(\"Group By Traversing Field\")\n        group_by = \"product__product_category\"  # Note the traversing\n\n\n.. _group_by_custom_querysets_topic:\n\nGroup by custom querysets\n-------------------------\n\nGrouping can also be over a curated queryset(s).\n\nExample:\n\n.. code-block:: python\n\n        class GroupByCustomQueryset(ReportView):\n            report_model = SalesTransaction\n            report_title = _(\"Group By Custom Queryset\")\n            date_field = \"date\"\n\n            group_by_custom_querysets = [\n                SalesTransaction.objects.filter(product__size__in=[\"big\", \"extra_big\"]),\n                SalesTransaction.objects.filter(product__size__in=[\"small\", \"extra_small\"]),\n                SalesTransaction.objects.filter(product__size=\"medium\"),\n            ]\n            group_by_custom_querysets_column_verbose_name = _(\"Product Size\")\n\n            columns = [\n                \"__index__\",\n                ComputationField.create(\n                    Sum, \"value\", verbose_name=_(\"Total Sold $\"), name=\"value\"\n                ),\n            ]\n\n            chart_settings = [\n                Chart(\n                    title=\"Total sold By Size $\",\n                    type=Chart.PIE,\n                    data_source=[\"value\"],\n                    title_source=[\"__index__\"],\n                ),\n                Chart(\n                    title=\"Total sold By Size $\",\n                    type=Chart.BAR,\n                    data_source=[\"value\"],\n                    title_source=[\"__index__\"],\n                ),\n            ]\n\n            def format_row(self, row_obj):\n                # Put the verbose names we need instead of the integer index\n                index = row_obj[\"__index__\"]\n                if index == 0:\n                    row_obj[\"__index__\"] = \"Big\"\n                elif index == 1:\n                    row_obj[\"__index__\"] = \"Small\"\n                elif index == 2:\n                    row_obj[\"__index__\"] = \"Medium\"\n                return row_obj\n\n\nThis report will create two groups, one for pending sales and another for paid and overdue together.\n\nThe ``__index__`` column is a \"magic\" column, it will added automatically to the report if it's not added.\nIt just hold the index of the row in the group.\nits verbose name (ie the one on the table header) can be customized via ``group_by_custom_querysets_column_verbose_name``\n\nYou can then customize the *value* of the __index__ column via ``format_row`` hook\n\n.. _no_group_by_topic:\n\nThe No Group By\n---------------\nSometimes you want to get some calculations done on the whole report_model, without a group_by.\nYou can do that by having the calculation fields you need in the columns, and leave out the group by.\n\nExample:\n\n.. code-block:: python\n\n    class NoGroupByReport(ReportView):\n        report_model = SalesTransaction\n        report_title = _(\"No-Group-By Report [WIP]\")\n        date_field = \"date\"\n        group_by = \"\"\n\n        columns = [\n            ComputationField.create(\n                method=Sum,\n                field=\"value\",\n                name=\"value__sum\",\n                verbose_name=\"Total sold $\",\n                is_summable=True,\n            ),\n        ]\n\nThis report will give one number, the sum of all the values in the ``value`` field of the ``SalesTransaction`` model, within a period.\n"
  },
  {
    "path": "docs/source/topics/index.rst",
    "content": ".. _topics:\n\nTopics\n======\n\nReportView is a ``django.views.generic.FromView`` subclass that exposes the **Report Generator API** allowing you to create a report seamlessly in a view.\n\n* Exposes the report generation options in the view class.\n* Auto generate the filter form based on the report model, or uses your custom form to generate and filter the report.\n* Return an html page prepared to display the results in a table and charts.\n* Export to CSV, which is extendable to apply other exporting methods. (like yaml or other)\n* Print the report in a dedicated page design.\n\n\nYou 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:\n\n#. Each type of the reports and its options.\n#. The general options available for all report types\n#. How to customize the Form\n#. How to customize exports and print.\n\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Topics:\n   :titlesonly:\n\n\n   group_by_report\n   time_series_options\n   crosstab_options\n   list_report_options\n   filter_form\n   widgets\n   integrating_slick_reporting\n   charts\n   exporting\n   computation_field\n   dynamic_model\n   pivot_report\n"
  },
  {
    "path": "docs/source/topics/integrating_slick_reporting.rst",
    "content": "Integrating reports into your front end\n=======================================\n\nTo integrate Slick Reporting into your application, you need to do override \"slick_reporting/base.html\" template,\nand/or, for more fine control over the report layout, override \"slick_reporting/report.html\" template.\n\nExample 1: Override base.html\n\n.. code-block:: html+django\n\n        {% extends \"base.html\" %}\n\n        {% block meta_page_title %} {{ report_title }}{% endblock %}\n        {% block page_title %} {{ report_title }} {% endblock %}\n\n        {% block extrajs %}\n            {{ block.super }}\n            {% include \"slick_reporting/js_resources.html\" %}\n        {% endblock %}\n\n\n\nLet's see what we did there\n1. We made our slick_reporting/base.html extend the main base.html\n2. We added the ``report_title`` context variable (which hold the current report title) to the meta_page_title and page_title blocks.\n   Use your version of these blocks, you might have them named differently.\n3. 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.\n   Also, use your version of the extrajs block. You might have it named differently.\n\nAnd that's it ! You can now use slick_reporting in your application.\n\n\nExample 2: Override report.html\n\nMaybe you want to add some extra information to the report, or change the layout of the report.\nYou can do this by overriding the slick_reporting/report.html template.\n\nHere is how it looks like:\n\n.. code-block:: html+django\n\n    {% extends 'slick_reporting/base.html' %}\n    {% load crispy_forms_tags i18n %}\n\n    {% block content %}\n        <div class=\"col-12\">\n            {% if form %}\n                <form id=\"reportForm\" class=\"card\">\n                    <div class=\"card-header\">\n                        <h3 class=\"card-title\">{% trans \"Filters\" %}</h3>\n                    </div>\n                    <div class=\"card-body\">\n                        {% crispy form crispy_helper %}\n                    </div>\n                    <div class=\"card-footer text-end\">\n                        <input type=\"submit\" value=\"{% trans \"Filter\" %}\"\n                               class=\"btn btn-primary  refreshReport\">\n                        <input type=\"button\" value=\"{% trans \"Export CSV\" %}\" class=\"btn btn-secondary exportCsvBtn\">\n                    </div>\n                </form>\n            {% endif %}\n\n            <div class=\"card\" id=\"{{ report_data.report_slug }}\">\n                <div class=\"card-header\">\n                    <h5 class=\"card-title\">{% trans \"Results\" %}</h5>\n                </div>\n                <div class=\"card-body\">\n                    <div data-report-widget\n                         data-report-url=\"{{ request.path }}\"\n                         data-extra-params=\"\"\n                         data-form-selector=\"#reportForm\"\n                            {% if not auto_load %} data-no-auto-load{% endif %}\n                         data-display-chart-selector=\"True\">\n                        <div data-report-chart>\n                        </div>\n                        <div data-report-table>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    {% endblock %}\n\n\nIntegrating reports into your Admin site\n=========================================\n\n1. Most probably you would want to override the default admin to add the extra report urls\nhttps://docs.djangoproject.com/en/4.2/ref/contrib/admin/#overriding-the-default-admin-site\n\n2. Add the report url to your admin site main get_urls\n\n.. code-block:: python\n\n    class CustomAdminAdminSite(admin.AdminSite):\n        def get_urls(self):\n            from my_apps.reports import MyAwesomeReport\n\n            urls = super().get_urls()\n            urls = [\n                path(\n                    \"reports/my-awesome-report/\",\n                    MyAwesomeReport.as_view(),\n                    name=\"my-awesome-report\",\n                ),\n            ] + urls\n            return urls\n\nNote that you need to add the reports urls to the top, or else the wildcard catch will raise a 404\n\n3. Override slick_reporting/base.html to extend the admin site\n\n.. code-block:: html+django\n\n    {% extends 'admin/base_site.html' %}\n    {% load i18n static slick_reporting_tags %}\n\n    {% block title %}{{ report_title }}{% endblock %}\n\n    {% block extrahead %}\n        {% include \"slick_reporting/js_resources.html\" %}\n        {% get_charts_media \"all\" %}\n    {% endblock %}\n\n    {% block breadcrumbs %}\n        <ul class=\"breadcrumb heading-text\">\n            <a href=\"{% url 'admin:index' %}\" class=\"breadcrumb-item\">\n                <i class=\"icon-home2 mx-2\"></i> {% trans 'Home' %} </a>\n            <a class=\"breadcrumb-item\"> {% trans 'Reports' %}</a>\n            <a class=\"breadcrumb-item\"> {{ report_title }}</a>\n        </ul>\n    {% endblock %}\n\n\n4. 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\n\n\n\n\n\n"
  },
  {
    "path": "docs/source/topics/list_report_options.rst",
    "content": ".. _list_reports:\n\nList Reports\n============\n\n\nIt's a simple ListView / admin changelist like report to display data in a model.\nIt's quite similar to ReportView except there is no calculation by default.\n\nHere is the options you can use to customize the report:\n\n#. ``columns``: a list of report_model fields to be displayed in the report, which support traversing\n\n.. code-block:: python\n\n    class RequestLog(ListReportView):\n        report_model = SalesTransaction\n        columns = [\n            \"id\",\n            \"date\",\n            \"client__name\",\n            \"product__name\",\n            \"quantity\",\n            \"price\",\n            \"value\",\n        ]\n\n\n#. ``filters``: a list of report_model fields to be used as filters.\n\n.. code-block:: python\n\n    class RequestLog(ListReportView):\n        report_model = SalesTransaction\n        columns = [\n            \"id\",\n            \"date\",\n            \"client__name\",\n            \"product__name\",\n            \"quantity\",\n            \"price\",\n            \"value\",\n        ]\n\n        filters = [\"product\", \"client\"]\n\n\n\n"
  },
  {
    "path": "docs/source/topics/pivot_report.rst",
    "content": ".. _pivot_report_topic:\n\n============================\nPrecomputed Crosstab Reports\n============================\n\nGeneral use case\n----------------\n\nPrecomputed crosstab reports are designed for **pre-computed or pre-aggregated data** stored as rows in the database.\nInstead of aggregating raw transactions at report time (like regular crosstab reports do),\nsetting ``crosstab_precomputed = True`` reads existing values and spreads a field's distinct values into columns.\n\nThis is common with:\n\n* Materialized views or summary tables\n* Data warehouse fact tables\n* ETL pipeline outputs\n* Pre-aggregated reporting tables\n* Legacy tables with denormalized data\n\nExample source data:\n\n+------------+------------+-------------+----------------+\n| product_id | month      | total_sales | total_quantity |\n+============+============+=============+================+\n| 1          | 2024-01-01 | 500         | 10             |\n+------------+------------+-------------+----------------+\n| 1          | 2024-02-01 | 600         | 12             |\n+------------+------------+-------------+----------------+\n| 2          | 2024-01-01 | 300         | 5              |\n+------------+------------+-------------+----------------+\n| 2          | 2024-02-01 | 400         | 8              |\n+------------+------------+-------------+----------------+\n\nThe precomputed crosstab report transforms this into:\n\n+------------+-------------------+-------------------+\n| product_id | total_sales Jan   | total_sales Feb   |\n+============+===================+===================+\n| 1          | 500               | 600               |\n+------------+-------------------+-------------------+\n| 2          | 300               | 400               |\n+------------+-------------------+-------------------+\n\n\nHow it relates to regular crosstab\n----------------------------------\n\nPrecomputed crosstab is a mode of the standard crosstab feature. Both spread a field's distinct values as columns.\nThe difference:\n\n* **Regular crosstab** (``crosstab_precomputed = False``) aggregates raw data per crosstab value using ``ComputationField``\n* **Precomputed crosstab** (``crosstab_precomputed = True``) reads pre-existing values from the database — no aggregation\n\nBoth modes use the same metadata and frontend integration, so all existing chart types\n(bar, line, pie, area) work with precomputed crosstab data.\n\n\nBasic example\n-------------\n\n.. code-block:: python\n\n    from slick_reporting.views import ReportView, Chart\n\n    class MonthlyProductSales(ReportView):\n        report_model = MonthlySummary\n        date_field = \"month\"\n        group_by = \"product_id\"\n        crosstab_field = \"month\"\n        crosstab_columns = [\"total_sales\", \"total_quantity\"]\n        crosstab_precomputed = True\n        columns = [\"product_id\", \"__crosstab__\"]\n\n        chart_settings = [\n            Chart(\n                \"Monthly Sales\",\n                Chart.LINE,\n                data_source=[\"total_sales\"],\n                title_source=[\"product_id\"],\n            ),\n        ]\n\nKey attributes:\n\n* ``crosstab_precomputed = True`` — read pre-computed values instead of aggregating\n* ``crosstab_field`` — the column whose distinct values become the dynamic columns\n* ``crosstab_columns`` — list of database column name strings to read values from\n* ``columns`` — use ``\"__crosstab__\"`` as a placeholder for where crosstab columns appear\n\n\nCrosstab by a non-date field\n-----------------------------\n\nThe crosstab field doesn't have to be a date — it can be any column:\n\n.. code-block:: python\n\n    class SalesByRegion(ReportView):\n        report_model = RegionalSummary\n        group_by = \"product_id\"\n        crosstab_field = \"region\"\n        crosstab_columns = [\"total_sales\"]\n        crosstab_precomputed = True\n        columns = [\"product_id\", \"__crosstab__\"]\n\nThis produces one row per product with a column for each region.\n\n.. note::\n\n    When multiple source rows exist for the same ``(group_by, crosstab_field)`` combination,\n    the last row encountered is used. Precomputed crosstab does **not** aggregate — if you\n    need aggregation, use a standard crosstab (without ``crosstab_precomputed``) or time series report instead.\n\n\nUsing with dynamic models\n--------------------------\n\nPrecomputed crosstab reports pair naturally with ``table_name`` for tables without a Django model:\n\n.. code-block:: python\n\n    class WarehouseReport(ReportView):\n        table_name = \"warehouse_monthly_sales\"\n        date_field = \"period_date\"\n        group_by = \"sku\"\n        crosstab_field = \"period_date\"\n        crosstab_columns = [\"revenue\", \"units_sold\"]\n        crosstab_precomputed = True\n        columns = [\"sku\", \"__crosstab__\"]\n\nSee :ref:`dynamic_model_topic` for more on reporting from arbitrary database tables.\n\n\nUsing with ReportGenerator directly\n------------------------------------\n\n.. code-block:: python\n\n    from slick_reporting.generator import ReportGenerator\n\n    report = ReportGenerator(\n        report_model=MonthlySummary,\n        group_by=\"product_id\",\n        date_field=\"month\",\n        crosstab_field=\"month\",\n        crosstab_columns=[\"total_sales\"],\n        crosstab_precomputed=True,\n        columns=[\"product_id\", \"__crosstab__\"],\n        start_date=datetime.datetime(2024, 1, 1),\n        end_date=datetime.datetime(2024, 6, 30),\n    )\n    data = report.get_report_data()\n\n\nDate filtering\n--------------\n\nWhen ``date_field`` is set, the precomputed crosstab respects ``start_date`` and ``end_date`` filters.\nOnly rows within the date range are included, and only crosstab values found in those rows\nappear as columns.\n\n\nCrosstab values with spaces or special characters\n--------------------------------------------------\n\nCrosstab values like ``\"New York\"`` or ``\"Q1/2024\"`` are sanitized for use in column names\n(non-alphanumeric characters replaced with underscores). The ``verbose_name`` preserves the\noriginal value for display in tables and charts.\n\nFor example, a crosstab value of ``\"New York\"`` produces:\n\n* Column name: ``total_salesCTNew_York`` (sanitized, used internally)\n* Verbose name: ``total_sales New York`` (original, displayed to users)\n\n\nLimitations\n-----------\n\n* **No aggregation.** Precomputed crosstab reads pre-existing values. If you need computation (Sum, Count, etc.),\n  use a standard crosstab without ``crosstab_precomputed``.\n\n* **Last value wins.** If multiple rows share the same ``(group_by, crosstab_field)`` value,\n  the last row's values are used.\n\n* **No FK traversal.** When using dynamic models, foreign key columns are plain integers.\n  Group by the column directly (e.g. ``product_id``) rather than traversing (``product__name``).\n"
  },
  {
    "path": "docs/source/topics/structure.rst",
    "content": ".. _structure:\n\n================\nRows and columns\n================\n\nIt's natural to think of a report as a form of tabular data, with rows and columns.\n\nWe willexplore the ways one can create the rows and column of a report.\n\na simple example\n"
  },
  {
    "path": "docs/source/topics/time_series_options.rst",
    "content": ".. _time_series:\n\nTime Series Reports\n===================\n\nA Time series report is a report that is generated for a periods of time.\nThe period can be daily, weekly, monthly, yearly or custom, calculations will be performed for each period in the time series.\n\nGeneral use case\n----------------\n\nHere is a quick look at the general use case\n\n\n.. code-block:: python\n\n        from django.utils.translation import gettext_lazy as _\n        from django.db.models import Sum\n        from slick_reporting.views import ReportView\n\n\n        class TimeSeriesReport(ReportView):\n            report_model = SalesTransaction\n            group_by = \"client\"\n\n            time_series_pattern = \"monthly\"\n            # options are: \"daily\", \"weekly\", \"bi-weekly\", \"monthly\", \"quarterly\", \"semiannually\", \"annually\" and \"custom\"\n\n            date_field = \"date\"\n\n            # These columns will be calculated for each period in the time series.\n            time_series_columns = [\n                ComputationField.create(Sum, \"value\", verbose_name=_(\"Sales For Month\")),\n            ]\n\n            columns = [\n                \"name\",\n                \"__time_series__\",\n                # This is the same as the time_series_columns, but this one will be on the whole set\n                ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Sales\")),\n            ]\n\n            chart_settings = [\n                Chart(\n                    \"Client Sales\",\n                    Chart.BAR,\n                    data_source=[\"sum__value\"],\n                    title_source=[\"name\"],\n                ),\n                Chart(\n                    \"Total Sales Monthly\",\n                    Chart.PIE,\n                    data_source=[\"sum__value\"],\n                    title_source=[\"name\"],\n                    plot_total=True,\n                ),\n                Chart(\n                    \"Total Sales [Area chart]\",\n                    Chart.AREA,\n                    data_source=[\"sum__value\"],\n                    title_source=[\"name\"],\n                ),\n            ]\n\n\nAllowing the User to Choose the time series pattern\n---------------------------------------------------\n\nYou can allow the User to Set the Pattern for the report , Let's create another version of the above report\nwhere the user can choose the pattern\n\n.. code-block:: python\n\n        class TimeSeriesReportWithSelector(TimeSeriesReport):\n            report_title = _(\"Time Series Report With Pattern Selector\")\n            time_series_selector = True\n            time_series_selector_choices = (\n                (\"daily\", _(\"Daily\")),\n                (\"weekly\", _(\"Weekly\")),\n                (\"bi-weekly\", _(\"Bi-Weekly\")),\n                (\"monthly\", _(\"Monthly\")),\n            )\n            time_series_selector_default = \"bi-weekly\"\n\n            time_series_selector_label = _(\"Period Pattern\")\n            # The label for the time series selector\n\n            time_series_selector_allow_empty = True\n            # Allow the user to select an empty time series, in which case no time series will be applied to the report.\n\n\nSet Custom Dates for the Time Series\n------------------------------------\n\nYou might want to set irregular pattern for the time series,\nLike first 10 days of each month , or the 3 summer month of every year.\n\nLet's see how you can do that, inheriting from teh same Time series we did first.\n\n.. code-block:: python\n\n\n        def get_current_year():\n            return datetime.datetime.now().year\n\n\n        class TimeSeriesReportWithCustomDates(TimeSeriesReport):\n            report_title = _(\"Time Series Report With Custom Dates\")\n            time_series_pattern = \"custom\"\n            time_series_custom_dates = (\n                (\n                    datetime.datetime(get_current_year(), 1, 1),\n                    datetime.datetime(get_current_year(), 1, 10),\n                ),\n                (\n                    datetime.datetime(get_current_year(), 2, 1),\n                    datetime.datetime(get_current_year(), 2, 10),\n                ),\n                (\n                    datetime.datetime(get_current_year(), 3, 1),\n                    datetime.datetime(get_current_year(), 3, 10),\n                ),\n            )\n\n\n\nCustomize the Computation Field label\n-------------------------------------\nMaybe you want to customize how the title of the time series computation field.\nFor this you want to Subclass ``ComputationField``, where you can customize\nhow the title is created and use it in the time_series_column instead of the one created on the fly.\n\nExample:\n\n.. code-block:: python\n\n\n    class SumOfFieldValue(ComputationField):\n        # A custom computation Field identical to the one created like this\n        # Similar to `ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Sales\"))`\n\n        calculation_method = Sum\n        calculation_field = \"value\"\n        name = \"sum_of_value\"\n\n        @classmethod\n        def get_time_series_field_verbose_name(cls, date_period, index, dates, pattern):\n            # date_period: is a tuple (start_date, end_date)\n            # index is the  index of the current pattern in the patterns on the report\n            # dates: the whole dates we have on the reports\n            # pattern it's the pattern name, ex: monthly, daily, custom\n            return f\"First 10 days sales {date_period[0].month}-{date_period[0].year}\"\n\n\n    class TimeSeriesReportWithCustomDatesAndCustomTitle(TimeSeriesReportWithCustomDates):\n        report_title = _(\"Time Series Report With Custom Dates and custom Title\")\n\n        time_series_columns = [\n            SumOfFieldValue,  # Use our newly created ComputationField with the custom time series verbose name\n        ]\n\n        chart_settings = [\n            Chart(\n                \"Client Sales\",\n                Chart.BAR,\n                data_source=[\n                    \"sum_of_value\"\n                ],  # Note:  This is the name of our `TotalSalesField` `field\n                title_source=[\"name\"],\n            ),\n            Chart(\n                \"Total Sales [Pie]\",\n                Chart.PIE,\n                data_source=[\"sum_of_value\"],\n                title_source=[\"name\"],\n                plot_total=True,\n            ),\n        ]\n\n\nTime Series without a group by\n------------------------------\nMaybe you want to get the time series calculated on the whole set, without grouping by anything.\nYou can do that by omitting the `group_by` attribute, and having only time series (or other computation fields) columns.\n\nExample:\n\n.. code-block:: python\n\n    class TimeSeriesWithoutGroupBy(ReportView):\n        report_title = _(\"Time Series without a group by\")\n        report_model = SalesTransaction\n        time_series_pattern = \"monthly\"\n        date_field = \"date\"\n        time_series_columns = [\n            ComputationField.create(Sum, \"value\", verbose_name=_(\"Sales For \")),\n        ]\n\n        columns = [\n            \"__time_series__\",\n            ComputationField.create(Sum, \"value\", verbose_name=_(\"Total Sales\")),\n        ]\n\n        chart_settings = [\n            Chart(\n                \"Total Sales [Bar]\",\n                Chart.BAR,\n                data_source=[\"sum__value\"],\n                title_source=[\"name\"],\n            ),\n            Chart(\n                \"Total Sales [Pie]\",\n                Chart.PIE,\n                data_source=[\"sum__value\"],\n                title_source=[\"name\"],\n            ),\n        ]\n\n\n\n\n.. _time_series_options:\n\nTime Series Options\n-------------------\n\n.. attribute:: ReportView.time_series_pattern\n\n            the time series pattern to be used in the report, it can be one of the following:\n            Possible options are: daily, weekly, semimonthly, monthly, quarterly, semiannually, annually and custom.\n            if `custom` is set, you'd need to override  `time_series_custom_dates`\n\n.. attribute:: ReportView.time_series_custom_dates\n\n            A list of tuples of (start_date, end_date) pairs indicating the start and end of each period.\n\n.. attribute:: ReportView.time_series_columns\n\n            a list of Calculation Field names which will be included in the series calculation.\n\n            .. code-block:: python\n\n                    class MyReport(ReportView):\n\n                        time_series_columns = [\n                            ComputationField.create(\n                                Sum, \"value\", verbose_name=_(\"Value\"), is_summable=True, name=\"sum__value\"\n                            ),\n                            ComputationField.create(\n                                Avg, \"Price\", verbose_name=_(\"Avg Price\"), is_summable=False\n                            ),\n                        ]\n\n\n\n\n\nLinks to demo\n''''''''''''''\n\nTime series Selector pattern `Demo <https://my-shop.django-erp-framework.com/reports/general_reports/profitabilityreportmonthly/>`_\nand the `Code on github <https://github.com/RamezIssac/my-shop/blob/main/general_reports/reports.py#L44>`_ for it.\n"
  },
  {
    "path": "docs/source/topics/widgets.rst",
    "content": ".. _widgets:\n.. _dashboard:\n\nDashboards\n==========\nYou can use the report data and charts on any other page, for example to create a dashboard.\nA dashboard page is a collection of report results  / charts / tables.\n\nAdding a widget to a page is as easy as this code\n\n.. code-block:: html+django\n\n        {% load static slick_reporting_tags %}\n\n        {% block content %}\n            <div class=\"div-holder\">\n                {% get_widget_from_url url_name=\"product-sales\" %}\n            </div>\n        {% endblock %}\n\n        {% block extrajs %}\n            {% include \"slick_reporting/js_resources.html\" %}\n            {% get_charts_media \"all\" %}\n        {% endblock %}\n\nThe `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.\n\nArguments\n---------\n\n* title: string, a title for the widget, default to the report title.\n* chart_id: the id of the chart that will be rendered as default.\n  chart_id is, by default, its index in the ``chart_settings`` list.\n* display_table: bool, If the widget should show the results table.\n* display_chart: bool, If the widget should show the chart.\n* display_chart_selector: bool, If the widget should show the chart selector links or just display the default,or the set chart_id, chart.\n* success_callback: string, the name of a javascript function that will be called after the report data is retrieved.\n* failure_callback: string, the name of a javascript function that will be called if the report data retrieval fails.\n* template_name: string, the template name used to render the widget. Default to `slick_reporting/widget_template.html`\n* extra_params: string, extra parameters to pass to the report.\n* 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.\n\n\nThis code above will be actually rendered as this in the html page:\n\n.. code-block:: html+django\n\n            <div class=\"card\">\n                    <div class=\"card-body\">\n                        <div data-report-widget <!-- The arguments passed will appear here as data-* attributes  --> >\n\n                            <!-- container for the chart -->\n                            <div id=\"container\" data-report-chart %>\n                            </div>\n\n                            <!-- container for the table -->\n                            <div data-report-table>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n\nThe ``data-report-widget`` attribute is used by the javascript to find the widget and render the report.\nThe ``data-report-chart`` attribute is used by the javascript to find the chart container and render the chart and the chart selector.\nThe ``data-report-table`` attribute is used by the javascript to find the table container and render the table.\n\n\nCustomization Example\n---------------------\n\nYou You can customize how the widget is loading by defining your own success call-back\nand fail call-back functions.\n\nThe success call-back function will receive the report data as a parameter\n\n\n.. code-block:: html+django\n\n        {% load i18n static slick_reporting_tags %}\n\n        {% get_widget_from_url url_name=\"product-sales\" success_callback=my_success_callback %}\n\n        <script>\n            function my_success_callback(data, $element) {\n                $element.html(data);\n                console.log(data);\n            }\n        </script>\n\n\nLive example:\n-------------\n\nYou can see a live example of the widgets in the `Demo project- Dashboard Page <https://django-slick-reporting.com/dashboard/>`_.\n"
  },
  {
    "path": "docs/source/tour.rst",
    "content": ".. _usage:\n\nA walk through\n==============\n\nUpdate\n~~~~~~\nYou can now go to https://django-slick-reporting for a better and practical guidance on the types of reports and what you can do.\n\n\n\nGiven that you have a model where there are data stored which you want to generate reports on.\nConsider below SalesOrder model example.\n\n+------------+------------+-----------+----------+-------+-------+\n| order_date | product_id | client_id | quantity | price | value |\n+------------+------------+-----------+----------+-------+-------+\n| 2019-01-01 | 1          | 1         | 5        | 15    | 75    |\n+------------+------------+-----------+----------+-------+-------+\n| 2019-02-15 | 2          | 2         | 7        | 20    | 140   |\n+------------+------------+-----------+----------+-------+-------+\n| 2019-02-20 | 2          | 1         | 5        | 20    | 100   |\n+------------+------------+-----------+----------+-------+-------+\n| 2019-03-14 | 1          | 2         | 3        | 15    | 45    |\n+------------+------------+-----------+----------+-------+-------+\n\nSlick Reporting help us answer some questions, like:\n\n* 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)\n* How much each product was sold or How much each Client bought? Filter by date range / client(s) / product(s)\n* How well each product sales is doing, monthly?\n* How client 1 compared with client 2,  compared with the rest of clients, on each product sales ?\n* How many orders were created a day ?\n\nTo answer those question, We can identify basic kind of alteration / calculation on the data\n\n\n1. Basic filtering\n------------------\n\nStart small,\nA ReportView like the below\n\n.. code-block:: python\n\n    # in your urls.py\n    path(\"path-to-report\", TransactionsReport.as_view())\n\n    # in your views.py\n    from slick_reporting.views import ReportView\n\n\n    class TransactionsReport(ReportView):\n        report_model = MySalesItem\n        columns = [\n            \"order_date\",\n            \"product__name\",\n            \"client__name\",\n            \"quantity\",\n            \"price\",\n            \"value\",\n        ]\n\n\nwill yield a Page with a nice filter form with\nA report where it displays the data as is but with filters however we can apply date and other filters\n\n+------------+---------------+-------------+----------+-------+-------+\n| order_date | Product Name  | Client Name | quantity | price | value |\n+------------+---------------+-------------+----------+-------+-------+\n| 2019-01-01 | Product 1     | Client 1    | 5        | 15    | 75    |\n+------------+---------------+-------------+----------+-------+-------+\n| 2019-02-15 | Product 2     | Client 2    | 7        | 20    | 140   |\n+------------+---------------+-------------+----------+-------+-------+\n| 2019-02-20 | Product 2     | Client 1    | 5        | 20    | 100   |\n+------------+---------------+-------------+----------+-------+-------+\n| 2019-03-14 | Product 1     | Client 2    | 3        | 15    | 45    |\n+------------+---------------+-------------+----------+-------+-------+\n\n2. Group By report\n-------------------\n\nWhere we can group by product -for example- and sum the quantity, or value sold.\n\n+-----------+----------------+-------------+\n| Product   | Total Quantity | Total Value |\n+-----------+----------------+-------------+\n| Product 1 | 8              | 120         |\n+-----------+----------------+-------------+\n| Product 2 | 13             | 240         |\n+-----------+----------------+-------------+\n\nwhich can be written like this:\n\n.. code-block:: python\n\n        class TotalQuanAndValueReport(ReportView):\n            report_model = MySalesItem\n            group_by = \"product\"\n            columns = [\"name\", \"__total_quantity__\", \"__total__\"]\n\n\n\n3. Time Series report\n------------------------\n\nwhere we can say how much sum of the quantity sold over a chunks of time periods (like weekly, monthly, ... )\n\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n| Product Name | SKU                  | Total Quantity  | Total Quantity | Total Quantity in ... | Total Quantity in December 20 |\n|              |                      | in Jan 20       | in Feb 20      |                       |                               |\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n| Product 1    | <from product model> | 5               | 0              | ...                   | 14                            |\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n| Product 2    | <from product model> | 0               | 13             | ...                   | 12                            |\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n| Product 3    | <from product model> | 17              | 12             | ...                   | 17                            |\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n\ncan be written like this\n\n.. code-block:: python\n\n        class TotalQuantityMonthly(ReportView):\n            report_model = MySalesItem\n            group_by = \"product\"\n            columns = [\"name\", \"sku\"]\n\n            time_series_pattern = \"monthly\"\n            time_series_columns = [\"__total_quantity__\"]\n\n\n4. Cross tab report\n--------------------\n\nWhere we can cross product sold over client for example\n\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n| Product Name | SKU                  | Client 1        | Client 2       | Client (n)            | The Reminder                  |\n|              |                      | Total value     | Total Value    |                       |                               |\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n| Product 1    | <from product model> | 10              | 15             | ...                   | 14                            |\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n| Product 2    | <from product model> | 11              | 12             | ...                   | 12                            |\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n| Product 3    | <from product model> | 17              | 12             | ...                   | 17                            |\n+--------------+----------------------+-----------------+----------------+-----------------------+-------------------------------+\n\nWhich can be written like this\n\n.. code-block:: python\n\n    class CrosstabProductClientValue(ReportView):\n        report_model = MySalesItem\n        group_by = \"product\"\n        columns = [\"name\", \"sku\"]\n\n        crosstab_model = \"client\"\n        crosstab_columns = [\"__total_value__\"]\n        crosstab_ids = [client1.pk, client2.pk, client3.pk]\n        crosstab_compute_remainder = True\n\n\n\n5. Time series - Cross tab\n--------------------------\n (#2 & #3 together) Not support at the time.. but soon we hope.\n\n\n\n\n"
  },
  {
    "path": "docs/source/tutorial.rst",
    "content": ".. _tutorial:\n\n=========\nTutorial\n=========\n\nIn this tutorial we will go over how to create different reports using Slick Reporting and integrating them into your projects.\n\nLet' say you have a Sales Transaction model in your project. Schema looking like this:\n\n.. code-block:: python\n\n    from django.db import models\n    from django.utils.translation import gettext_lazy as _\n\n\n    class Client(models.Model):\n        name = models.CharField(_(\"Name\"), max_length=255)\n        country = models.CharField(_(\"Country\"), max_length=255, default=\"US\")\n\n\n    class Product(models.Model):\n        name = models.CharField(_(\"Name\"), max_length=255)\n        sku = models.CharField(_(\"SKU\"), max_length=255)\n\n\n    class Sales(models.Model):\n        doc_date = models.DateTimeField(_(\"date\"), db_index=True)\n        client = models.ForeignKey(Client, on_delete=models.CASCADE)\n        product = models.ForeignKey(Product, on_delete=models.CASCADE)\n        quantity = models.DecimalField(\n            _(\"Quantity\"), max_digits=19, decimal_places=2, default=0\n        )\n        price = models.DecimalField(_(\"Price\"), max_digits=19, decimal_places=2, default=0)\n        value = models.DecimalField(_(\"Value\"), max_digits=19, decimal_places=2, default=0)\n\n\n\nNow, you want to extract the following information from that sales model, present to your users in a nice table and chart:\n\n#. Total sales per product.\n#. Total Sales per client country.\n#. Total sales per product each month.\n#. Total Sales per product and country.\n#. Total Sales per product and country, per month.\n#. Display last 10 sales transactions.\n\nGroup By Reports\n================\n\n1. Total sales per product\n--------------------------\n\nThis can be done via an SQL statement looking like this:\n\n.. code-block:: sql\n\n    SELECT product_id, SUM(value) FROM sales GROUP BY product_id;\n\nIn Slick Reporting, you can do the same thing by creating a report view looking like this:\n\n.. code-block:: python\n\n            # in views.py\n\n            from django.db.models import Sum\n            from slick_reporting.views import ReportView, Chart\n            from slick_reporting.fields import ComputationField\n            from .models import Sales\n\n\n            class TotalProductSales(ReportView):\n                report_model = SalesTransaction\n                date_field = \"date\"\n                group_by = \"product\"\n                columns = [\n                    \"name\",\n                    ComputationField.create(\n                        Sum, \"quantity\", verbose_name=\"Total quantity sold\", is_summable=False\n                    ),\n                    ComputationField.create(\n                        Sum, \"value\", name=\"sum__value\", verbose_name=\"Total Value sold $\"\n                    ),\n                ]\n\n                chart_settings = [\n                    Chart(\n                        \"Total sold $\",\n                        Chart.BAR,\n                        data_source=[\"sum__value\"],\n                        title_source=[\"name\"],\n                    ),\n                    Chart(\n                        \"Total sold $ [PIE]\",\n                        Chart.PIE,\n                        data_source=[\"sum__value\"],\n                        title_source=[\"name\"],\n                    ),\n                ]\n\nThen in your urls.py add the following:\n\n.. code-block:: python\n\n    from django.urls import path\n    from .views import TotalProductSales\n\n    urlpatterns = [\n        path(\n            \"total-product-sales/\", TotalProductSales.as_view(), name=\"total-product-sales\"\n        ),\n    ]\n\nNow visit the url ``/total-product-sales/`` and you will see the page report. Containing a Filter Form, the report table and a chart.\n\n\nYou can change the dates in the filter form , add some filters and the report will be updated.\nYou can also export the report to CSV.\n\n2. Total Sales per each client country\n--------------------------------------\n\n.. code-block:: python\n\n            # in views.py\n\n            from django.db.models import Sum\n            from slick_reporting.views import ReportView, Chart\n            from slick_reporting.fields import ComputationField\n            from .models import SalesTransaction\n\n\n            class TotalProductSalesByCountry(ReportView):\n                report_model = SalesTransaction\n                date_field = \"date\"\n                group_by = \"client__country\"  # notice the double underscore\n                columns = [\n                    \"client__country\",\n                    ComputationField.create(\n                        Sum,\n                        \"value\",\n                        name=\"sum__value\",\n                        verbose_name=\"Total Value sold by country $\",\n                    ),\n                ]\n\n                chart_settings = [\n                    Chart(\n                        \"Total sold by country $\",\n                        Chart.PIE,  # A Pie Chart\n                        data_source=[\"sum__value\"],\n                        title_source=[\"client__country\"],\n                    ),\n                ]\n\n\nTime Series Reports\n====================\nA time series report is a report that computes the data for each period of time. For example, if you want to see the total sales per each month, then you need to create a time series report.\n\n\n\n.. code-block:: python\n\n    from django.utils.translation import gettext_lazy as _\n    from slick_reporting.fields import ComputationField\n\n\n    class SumValueComputationField(ComputationField):\n        calculation_method = Sum\n        calculation_field = \"value\"\n        verbose_name = _(\"Sales Value\")\n        name = \"my_value_sum\"\n\n\n    class MonthlyProductSales(ReportView):\n        report_model = SalesTransaction\n        date_field = \"date\"\n        group_by = \"product\"\n        columns = [\"name\", \"sku\"]\n\n        time_series_pattern = \"monthly\"\n        time_series_columns = [\n            SumValueComputationField,\n        ]\n\n        chart_settings = [\n            Chart(\n                _(\"Total Sales Monthly\"),\n                Chart.PIE,\n                data_source=[\"my_value_sum\"],\n                title_source=[\"name\"],\n                plot_total=True,\n            ),\n            Chart(\n                _(\"Sales Monthly [Bar]\"),\n                Chart.COLUMN,\n                data_source=[\"my_value_sum\"],\n                title_source=[\"name\"],\n            ),\n        ]\n\nthen again in your urls.py add the following:\n\n.. code-block:: python\n\n    from django.urls import path\n    from .views import MonthlyProductSales\n\n    urlpatterns = [\n        path(\n            \"monthly-product-sales/\",\n            MonthlyProductSales.as_view(),\n            name=\"monthly-product-sales\",\n        ),\n    ]\n\nNote: We created SumValueComputationField to avoid repeating the same code in each report. You can create your own ``ComputationFields`` and use them in your reports.\n\nPretty Cool yes ?\n\nCrossTab Reports\n================\nA crosstab report shows the relation between two or more variables. For example, if you want to see the total sales per each product and country, then you need to create a crosstab report.\n\n.. code-block:: python\n\n\n    class ProductSalesPerCountryCrosstab(ReportView):\n        report_model = SalesTransaction\n        date_field = \"date\"\n        group_by = \"product\"\n        crosstab_field = \"client__country\"\n        crosstab_columns = [\n            SumValueComputationField,\n        ]\n\n        crosstab_ids = [\"US\", \"KW\", \"EG\", \"DE\"]\n        crosstab_compute_remainder = True\n\n        columns = [\n            \"name\",\n            \"sku\",\n            \"__crosstab__\",\n            SumValueComputationField,\n        ]\n\nThen again in your urls.py add the following:\n\n.. code-block:: python\n\n    from django.urls import path\n    from .views import MyCrosstabReport\n\n    urlpatterns = [\n        path(\n            \"product-sales-per-country/\",\n            ProductSalesPerCountryCrosstab.as_view(),\n            name=\"product-sales-per-country\",\n        ),\n    ]\n\n\nList Reports\n============\nA list report is a report that shows a list of records. For example, if you want to see the last 10 sales transactions, then you need to create a list report.\n\n.. code-block:: python\n\n    from slick_reporting.views import ListReportView\n\n\n    class LastTenSales(ListReportView):\n        report_model = SalesTransaction\n        report_title = \"Last 10 sales\"\n        date_field = \"date\"\n        filters = [\"product\", \"client\", \"date\"]\n        columns = [\n            \"product__name\",\n            \"client__name\",\n            \"date\",\n            \"quantity\",\n            \"price\",\n            \"value\",\n        ]\n        default_order_by = \"-date\"\n        limit_records = 10\n\n\n\n\nThen again in your urls.py add the following:\n\n.. code-block:: python\n\n    from django.urls import path\n    from .views import LastTenSales\n\n    urlpatterns = [\n        path(\n            \"last-ten-sales/\",\n            LastTenSales.as_view(),\n            name=\"last-ten-sales\",\n        ),\n    ]\n\nIntegrate the view in your project\n===================================\n\nYou can use the template in your own project by following these steps:\n\n#. Override ``slick_reporting/base.html`` in your own project and make it extends you own base template.\n#. Make sure your base template has a ``{% block content %}`` block and a  ``{% block extrajs %}`` block.\n#. Add the slick reporting js resources to the page by adding `{% include \"slick_reporting/js_resources.html\" %}` to an appropriate block.\n\n\nOverriding the Form\n===================\n\nThe system expect that the form used with the ``ReportView`` to implement the ``slick_reporting.forms.BaseReportForm`` interface.\n\nThe 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.\n\n\n* ``get_filters``: Mandatory, return a tuple (Q_filters , kwargs filter) to be used in filtering.\n  q_filter: can be none or a series of Django's Q queries\n  kwargs_filter: None or a dictionary of filters\n\n* ``get_start_date``: Mandatory, return the start date of the report.\n\n* ``get_end_date``: Mandatory, return the end date of the report.\n\n\nFor detailed information about the form, please check :ref:`filter_form`\n\nExample\n-------\n\n.. code-block:: python\n\n    # forms.py\n    from django import forms\n    from django.db.models import Q\n    from slick_reporting.forms import BaseReportForm\n\n    # A Normal form , Inheriting from BaseReportForm\n    class TotalSalesFilterForm(BaseReportForm, forms.Form):\n        PRODUCT_SIZE_CHOICES = (\n            (\"all\", \"All\"),\n            (\"big-only\", \"Big Only\"),\n            (\"small-only\", \"Small Only\"),\n            (\"medium-only\", \"Medium Only\"),\n            (\"all-except-extra-big\", \"All except extra Big\"),\n        )\n        start_date = forms.DateField(\n            required=False,\n            label=\"Start Date\",\n            widget=forms.DateInput(attrs={\"type\": \"date\"}),\n        )\n        end_date = forms.DateField(\n            required=False, label=\"End Date\", widget=forms.DateInput(attrs={\"type\": \"date\"})\n        )\n        product_size = forms.ChoiceField(\n            choices=PRODUCT_SIZE_CHOICES,\n            required=False,\n            label=\"Product Size\",\n            initial=\"all\",\n        )\n\n        def get_filters(self):\n            # return the filters to be used in the report\n            # Note: the use of Q filters and kwargs filters\n            kw_filters = {}\n            q_filters = []\n            if self.cleaned_data[\"product_size\"] == \"big-only\":\n                kw_filters[\"product__size__in\"] = [\"extra_big\", \"big\"]\n            elif self.cleaned_data[\"product_size\"] == \"small-only\":\n                kw_filters[\"product__size__in\"] = [\"extra_small\", \"small\"]\n            elif self.cleaned_data[\"product_size\"] == \"medium-only\":\n                kw_filters[\"product__size__in\"] = [\"medium\"]\n            elif self.cleaned_data[\"product_size\"] == \"all-except-extra-big\":\n                q_filters.append(~Q(product__size__in=[\"extra_big\", \"big\"]))\n            return q_filters, kw_filters\n\n        def get_start_date(self):\n            return self.cleaned_data[\"start_date\"]\n\n        def get_end_date(self):\n            return self.cleaned_data[\"end_date\"]\n\n\n\nRecap\n=====\nIn the tutorial we went over how to create a report using the ``ReportView`` and ``ListReportView`` classes.\nThe different types of reports we created are:\n\n1. Grouped By Reports\n2. Time Series Reports\n3. Crosstab Reports\n4. List Reports\n\nYou can create a report by inheriting from ``ReportView`` or ``ListReportView`` and setting the following attributes:\n\n* ``report_model``: The model to be used in the report\n* ``date_field``: The date field to be used in the report\n* ``columns``: The columns to be displayed in the report\n* ``default_order_by``: The default order by for the report\n* ``limit_records``: The limit of records to be displayed in the report\n* ``group_by``: The field to be used to group the report by\n* ``time_series_pattern``: The time series pattern to be used in the report\n* ``time_series_columns``: The columns to be displayed in the time series report\n* ``crosstab_field``: The field to be used to create a crosstab report\n* ``crosstab_columns``: The columns to be displayed in the crosstab report\n* ``crosstab_ids``: The ids to be used in the crosstab report\n* ``crosstab_compute_remainder``: Whether to compute the remainder in the crosstab report\n* ``chart_settings``: The chart settings to be used in the report\n\nWe also saw how you can customize the form used in the report by inheriting from ``BaseReportForm``, and integrating the view in your project.\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=61.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.black]\nline-length = 120\n\n\n[tool.ruff]\nline-length = 120"
  },
  {
    "path": "requirements.txt",
    "content": "Django\npython-dateutil>=2.8.1\npytz\nsimplejson\ndjango-crispy-forms"
  },
  {
    "path": "runtests.py",
    "content": "#!/usr/bin/env python\nimport os\nimport sys\n\nimport argparse\nimport django\nfrom django.conf import settings\nfrom django.test.utils import get_runner\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        description=\"Run the Django Slick Reporting test suite.\"\n    )\n    parser.add_argument(\n        \"modules\",\n        nargs=\"*\",\n        metavar=\"module\",\n        help='Optional path(s) to test modules; e.g. \"i18n\" or '\n        '\"i18n.tests.TranslationTests.test_lazy_objects\".',\n    )\n    options = parser.parse_args()\n\n    options.modules = [os.path.normpath(labels) for labels in options.modules]\n\n    os.environ[\"DJANGO_SETTINGS_MODULE\"] = \"tests.settings\"\n    django.setup()\n    TestRunner = get_runner(settings)\n    test_runner = TestRunner()\n    failures = test_runner.run_tests(options.modules)\n    # failures = test_runner.run_tests([\"tests\"])\n    sys.exit(bool(failures))\n"
  },
  {
    "path": "scripts/extract_changelog.py",
    "content": "#!/usr/bin/env python\n\"\"\"Extract a single version's section from CHANGELOG.md and print it to stdout.\n\nUsage: python scripts/extract_changelog.py 1.4.0\n\"\"\"\nimport re\nimport sys\n\n\ndef extract(version: str, changelog_path: str = \"CHANGELOG.md\") -> str:\n    with open(changelog_path) as f:\n        text = f.read()\n\n    # Match the heading for the requested version (e.g. ## [1.4.0] or ## [1.4.0] - 2026-05-01)\n    pattern = rf\"(^## \\[{re.escape(version)}\\][^\\n]*\\n)(.*?)(?=^## \\[|\\Z)\"\n    match = re.search(pattern, text, re.MULTILINE | re.DOTALL)\n    if not match:\n        print(f\"Version {version} not found in {changelog_path}\", file=sys.stderr)\n        sys.exit(1)\n\n    # Return the body without the heading line itself\n    return match.group(2).strip()\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) != 2:\n        print(f\"Usage: {sys.argv[0]} <version>\", file=sys.stderr)\n        sys.exit(1)\n    print(extract(sys.argv[1]))\n"
  },
  {
    "path": "setup.cfg",
    "content": "[metadata]\nlicense_file = LICENSE.md\nname = django-slick-reporting\nversion = attr: slick_reporting.__version__\nauthor = Ra Systems\nauthor_email = ramez@rasystems.io\ndescription = A one-stop report and analytics generation and computation with batteries included\nlong_description = file:README.rst\nlong_description_content_type = text/x-rst\nurl = https://django-slick-reporting.com/\nproject_urls =\n    Documentation = https://django-slick-reporting.readthedocs.io/en/latest/\n    Source = https://github.com/ra-systems/django-slick-reporting\n    Changelog = https://github.com/ra-systems/django-slick-reporting/blob/master/CHANGELOG.md\nclassifiers =\n    Environment :: Web Environment\n    Framework :: Django\n    Framework :: Django :: 4.2\n    Framework :: Django :: 5.0\n    Framework :: Django :: 5.1\n    Intended Audience :: Developers\n    Development Status :: 5 - Production/Stable\n    License :: OSI Approved :: BSD License\n    Natural Language :: English\n    Operating System :: MacOS :: MacOS X\n    Operating System :: POSIX\n    Operating System :: POSIX :: BSD\n    Operating System :: POSIX :: Linux\n    Operating System :: Microsoft :: Windows\n    Programming Language :: Python\n    Programming Language :: Python :: 3\n    Programming Language :: Python :: 3 :: Only\n    Programming Language :: Python :: 3.9\n    Programming Language :: Python :: 3.10\n    Programming Language :: Python :: 3.11\n    Programming Language :: Python :: 3.12\n    Topic :: Internet :: WWW/HTTP\n    Topic :: Internet :: WWW/HTTP :: Dynamic Content\n\n[options]\ninclude_package_data = true\npackages = find:\n\npython_requires = >=3.9\ninstall_requires =\n    django>=2.2\n    python-dateutil>2.8.1\n    pytz\n    simplejson\n    django-crispy-forms\n\n\n\n\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\n\nsetup()\n"
  },
  {
    "path": "slick_reporting/__init__.py",
    "content": "default_app_config = \"slick_reporting.apps.ReportAppConfig\"\n\nVERSION = (1, 4, 0)\n\n__version__ = \"1.4.0\"\n"
  },
  {
    "path": "slick_reporting/app_settings.py",
    "content": "from django.conf import settings\nfrom django.urls import get_callable\nfrom django.utils.functional import lazy\nfrom django.utils.translation import gettext_lazy as _\n\nimport datetime\n\n\ndef get_first_of_this_year():\n    d = datetime.datetime.today()\n    return datetime.datetime(d.year, 1, 1, 0, 0)\n\n\ndef get_end_of_this_year():\n    d = datetime.datetime.today()\n    return datetime.datetime(d.year + 1, 1, 1, 0, 0)\n\n\ndef get_start_date():\n    start_date = getattr(settings, \"SLICK_REPORTING_DEFAULT_START_DATE\", False)\n    return start_date or get_first_of_this_year()\n\n\ndef get_end_date():\n    end_date = getattr(settings, \"SLICK_REPORTING_DEFAULT_END_DATE\", False)\n    return end_date or datetime.datetime.today()\n\n\nSLICK_REPORTING_DEFAULT_START_DATE = lazy(get_start_date, datetime.datetime)()\nSLICK_REPORTING_DEFAULT_END_DATE = lazy(get_end_date, datetime.datetime)()\n\n\nSLICK_REPORTING_DEFAULT_CHARTS_ENGINE = getattr(settings, \"SLICK_REPORTING_DEFAULT_CHARTS_ENGINE\", \"highcharts\")\n\n\nSLICK_REPORTING_JQUERY_URL = getattr(\n    settings, \"SLICK_REPORTING_JQUERY_URL\", \"https://code.jquery.com/jquery-3.7.0.min.js\"\n)\n\n\nSLICK_REPORTING_SETTINGS_DEFAULT = {\n    \"JQUERY_URL\": SLICK_REPORTING_JQUERY_URL,\n    \"DEFAULT_START_DATE_TIME\": get_start_date(),\n    \"DEFAULT_END_DATE_TIME\": get_end_date(),\n    \"DEFAULT_CHARTS_ENGINE\": SLICK_REPORTING_DEFAULT_CHARTS_ENGINE,\n    \"MEDIA\": {\n        \"override\": False,\n        \"js\": (\n            \"https://cdn.jsdelivr.net/momentjs/latest/moment.min.js\",\n\n            \"https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js\",\n            \"https://cdn.datatables.net/1.13.4/js/dataTables.bootstrap5.min.js\",\n            \"slick_reporting/slick_reporting.js\",\n            \"slick_reporting/slick_reporting.report_loader.js\",\n            \"slick_reporting/slick_reporting.datatable.js\",\n        ),\n        \"css\": {\n            \"all\": (\n                \"https://cdn.datatables.net/1.13.4/css/dataTables.bootstrap5.min.css\",\n            )\n        },\n    },\n    \"FONT_AWESOME\": {\n        \"CSS_URL\": \"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css\",\n        \"ICONS\": {\n            \"pie\": \"fas fa-chart-pie\",\n            \"bar\": \"fas fa-chart-bar\",\n            \"line\": \"fas fa-chart-line\",\n            \"area\": \"fas fa-chart-area\",\n            \"column\": \"fas fa-chart-bar\",\n        },\n    },\n    \"CHARTS\": {\n        \"highcharts\": {\n            \"entryPoint\": \"$.slick_reporting.highcharts.displayChart\",\n            \"js\": (\n                \"https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js\",\n                \"slick_reporting/slick_reporting.highchart.js\",\n            ),\n        },\n        \"chartsjs\": {\n            \"entryPoint\": \"$.slick_reporting.chartsjs.displayChart\",\n            \"js\": (\"https://cdn.jsdelivr.net/npm/chart.js\", \"slick_reporting/slick_reporting.chartsjs.js\"),\n        },\n    },\n    \"MESSAGES\": {\n        \"total\": _(\"Total\"),\n        \"export_to_csv\": _(\"Export to CSV\"),\n        \"print_report\": _(\"Print\"),\n    },\n    \"REPORT_VIEW_ACCESS_FUNCTION\": \"slick_reporting.helpers.user_test_function\",\n}\n\n\ndef get_slick_reporting_settings():\n    slick_settings = SLICK_REPORTING_SETTINGS_DEFAULT.copy()\n    slick_chart_settings = slick_settings[\"CHARTS\"].copy()\n\n    user_settings = getattr(settings, \"SLICK_REPORTING_SETTINGS\", {})\n    user_chart_settings = user_settings.get(\"CHARTS\", {})\n\n    user_media_settings = user_settings.get(\"MEDIA\", {})\n    override_media = user_media_settings.get(\"override\", False)\n    if override_media:\n        slick_settings[\"MEDIA\"] = user_media_settings\n    else:\n        slick_settings[\"MEDIA\"][\"js\"] = slick_settings[\"MEDIA\"][\"js\"] + user_media_settings.get(\"js\", ())\n        slick_settings[\"MEDIA\"][\"css\"][\"all\"] = slick_settings[\"MEDIA\"][\"css\"][\"all\"] + user_media_settings.get(\n            \"css\", {}\n        ).get(\"all\", ())\n\n    slick_chart_settings.update(user_chart_settings)\n    slick_settings.update(user_settings)\n    slick_settings[\"CHARTS\"] = slick_chart_settings\n\n    # slick_settings = {**SLICK_REPORTING_SETTINGS_DEFAULT, **getattr(settings, \"SLICK_REPORTING_SETTINGS\", {})}\n    start_date = getattr(settings, \"SLICK_REPORTING_DEFAULT_START_DATE\", False)\n    end_date = getattr(settings, \"SLICK_REPORTING_DEFAULT_END_DATE\", False)\n    # backward compatibility, todo remove in next major release\n    if start_date:\n        slick_settings[\"DEFAULT_START_DATE_TIME\"] = start_date\n    if end_date:\n        slick_settings[\"DEFAULT_END_DATE_TIME\"] = end_date\n\n    return slick_settings\n\n\nSLICK_REPORTING_SETTINGS = lazy(get_slick_reporting_settings, dict)()\n\n\ndef get_media():\n    return SLICK_REPORTING_SETTINGS[\"MEDIA\"]\n\n\ndef get_access_function():\n    return get_callable(SLICK_REPORTING_SETTINGS[\"REPORT_VIEW_ACCESS_FUNCTION\"])\n"
  },
  {
    "path": "slick_reporting/apps.py",
    "content": "from django import apps\n\n\nclass ReportAppConfig(apps.AppConfig):\n    verbose_name = \"Slick Reporting\"\n    name = \"slick_reporting\"\n\n    def ready(self):\n        super().ready()\n        from . import fields  # noqa\n"
  },
  {
    "path": "slick_reporting/decorators.py",
    "content": "def report_field_register(report_field, *args, **kwargs):\n    \"\"\"\n    Registers the given model(s) classes and wrapped ModelAdmin class with\n    admin site:\n\n    @register(Author)\n    class AuthorAdmin(admin.ModelAdmin):\n        pass\n\n    A kwarg of `site` can be passed as the admin site, otherwise the default\n    admin site will be used.\n    \"\"\"\n    from .fields import ComputationField\n    from .registry import field_registry\n\n    def _model_admin_wrapper(admin_class):\n        if not issubclass(admin_class, ComputationField):\n            raise ValueError(\"Wrapped class must subclass ComputationField.\")\n\n        field_registry.register(report_field)\n\n    _model_admin_wrapper(report_field)\n    return report_field\n"
  },
  {
    "path": "slick_reporting/dynamic_model.py",
    "content": "from django.db import models, connections, OperationalError, ProgrammingError\n\n_model_cache = {}\n\nFIELD_TYPE_MAP = {\n    \"AutoField\": models.AutoField,\n    \"BigAutoField\": models.BigAutoField,\n    \"SmallAutoField\": models.SmallAutoField,\n    \"BigIntegerField\": models.BigIntegerField,\n    \"BooleanField\": models.BooleanField,\n    \"CharField\": models.CharField,\n    \"DateField\": models.DateField,\n    \"DateTimeField\": models.DateTimeField,\n    \"DecimalField\": models.DecimalField,\n    \"DurationField\": models.DurationField,\n    \"FloatField\": models.FloatField,\n    \"IntegerField\": models.IntegerField,\n    \"PositiveIntegerField\": models.PositiveIntegerField,\n    \"PositiveBigIntegerField\": models.PositiveBigIntegerField,\n    \"PositiveSmallIntegerField\": models.PositiveSmallIntegerField,\n    \"SmallIntegerField\": models.SmallIntegerField,\n    \"TextField\": models.TextField,\n    \"TimeField\": models.TimeField,\n    \"BinaryField\": models.BinaryField,\n    \"UUIDField\": models.UUIDField,\n    \"JSONField\": models.JSONField,\n    \"GenericIPAddressField\": models.GenericIPAddressField,\n    \"IPAddressField\": models.GenericIPAddressField,\n    \"SlugField\": models.SlugField,\n    \"URLField\": models.URLField,\n    \"FilePathField\": models.FilePathField,\n}\n\n\ndef _make_field(field_type_str, column_info, is_pk):\n    \"\"\"Create a Django field instance from introspection data.\"\"\"\n    field_class = FIELD_TYPE_MAP.get(field_type_str, models.TextField)\n    kwargs = {}\n\n    if is_pk:\n        if field_class in (models.AutoField, models.BigAutoField, models.SmallAutoField):\n            return field_class(primary_key=True)\n        kwargs[\"primary_key\"] = True\n    else:\n        if column_info.null_ok:\n            kwargs[\"null\"] = True\n            kwargs[\"blank\"] = True\n\n    if field_class == models.CharField:\n        max_length = column_info.display_size\n        if not max_length or max_length < 0 or max_length > 10000:\n            max_length = 255\n        kwargs[\"max_length\"] = max_length\n\n    elif field_class == models.DecimalField:\n        kwargs[\"max_digits\"] = column_info.precision if column_info.precision else 19\n        kwargs[\"decimal_places\"] = column_info.scale if column_info.scale else 2\n\n    elif field_class in (models.SlugField, models.URLField):\n        max_length = column_info.display_size\n        if not max_length or max_length < 0 or max_length > 10000:\n            max_length = 255 if field_class == models.SlugField else 200\n        kwargs[\"max_length\"] = max_length\n\n    return field_class(**kwargs)\n\n\ndef get_dynamic_model(table_name, database=\"default\", schema=None):\n    \"\"\"\n    Introspect a database table and return a Django model class for it.\n\n    The returned model is a real Django model with ``managed = False``,\n    so all ORM operations (filter, annotate, aggregate, etc.) work natively.\n    Results are cached so repeated calls for the same table return the same class.\n\n    Args:\n        table_name: The database table name to introspect.\n        database: The database alias to use (default: 'default').\n        schema: Optional database schema name (e.g. 'analytics').\n            On PostgreSQL, the schema must be in the connection's ``search_path``\n            for introspection to find the table. If provided, the model's\n            ``db_table`` will be set to ``\"schema\".\"table\"`` so ORM queries\n            reference the correct schema.\n\n    Returns:\n        A Django model class mapped to the given table.\n\n    Raises:\n        ValueError: If the table does not exist in the database.\n    \"\"\"\n    db_table = f'\"{schema}\".\"{table_name}\"' if schema else table_name\n    cache_key = f\"{database}:{db_table}\"\n    if cache_key in _model_cache:\n        return _model_cache[cache_key]\n\n    connection = connections[database]\n    try:\n        with connection.cursor() as cursor:\n            tables = connection.introspection.table_names(cursor)\n            if table_name not in tables:\n                raise ValueError(\n                    f\"Table '{table_name}' does not exist in the '{database}' database. \"\n                    f\"Available tables: {', '.join(sorted(tables))}\"\n                )\n\n            table_description = connection.introspection.get_table_description(cursor, table_name)\n            try:\n                pk_columns = connection.introspection.get_primary_key_columns(cursor, table_name)\n            except AttributeError:\n                # Fallback for older Django versions\n                pk_column = connection.introspection.get_primary_key_column(cursor, table_name)\n                pk_columns = [pk_column] if pk_column else []\n    except (OperationalError, ProgrammingError) as exc:\n        raise RuntimeError(\n            f\"slick_reporting: Could not introspect table '{table_name}' — \"\n            f\"the database is not ready (migrations may not have run yet). \"\n            f\"Original error: {exc}\"\n        ) from exc\n\n    fields = {}\n    has_pk = False\n\n    for col_info in table_description:\n        result = connection.introspection.get_field_type(col_info.type_code, col_info)\n        # get_field_type returns a string in modern Django, or (string, params) in older versions\n        if isinstance(result, tuple):\n            field_type_str = result[0]\n        else:\n            field_type_str = result\n        is_pk = col_info.name in pk_columns\n\n        if is_pk:\n            has_pk = True\n\n        field = _make_field(field_type_str, col_info, is_pk)\n        field.db_column = col_info.name\n        fields[col_info.name] = field\n\n    if not has_pk:\n        # Table has no PK — add a synthetic one on the first column\n        if table_description:\n            first_col = table_description[0].name\n            result = connection.introspection.get_field_type(\n                table_description[0].type_code, table_description[0]\n            )\n            ft = result[0] if isinstance(result, tuple) else result\n            fields[first_col] = _make_field(ft, table_description[0], is_pk=True)\n            fields[first_col].db_column = first_col\n\n    # Build a valid Python class name from the table name\n    name_parts = table_name.replace(\".\", \"_\").split(\"_\")\n    if schema:\n        name_parts = schema.replace(\".\", \"_\").split(\"_\") + name_parts\n    model_name = \"\".join(part.capitalize() for part in name_parts)\n    if not model_name:\n        model_name = \"DynamicModel\"\n\n    attrs = {\n        \"__module__\": __name__,\n        \"Meta\": type(\n            \"Meta\",\n            (),\n            {\n                \"managed\": False,\n                \"db_table\": db_table,\n                \"app_label\": \"slick_reporting\",\n            },\n        ),\n    }\n    attrs.update(fields)\n\n    model = type(model_name, (models.Model,), attrs)\n\n    # Register in Django's app registry\n    from django.apps import apps\n\n    app_models = apps.all_models.get(\"slick_reporting\", {})\n    model_key = model_name.lower()\n    if model_key in app_models:\n        # Already registered — reuse the existing model\n        model = app_models[model_key]\n    else:\n        apps.register_model(\"slick_reporting\", model)\n\n    _model_cache[cache_key] = model\n    return model\n"
  },
  {
    "path": "slick_reporting/fields.py",
    "content": "from __future__ import annotations\n\nfrom warnings import warn\n\nfrom django.db.models import Sum, Q\nfrom django.template.defaultfilters import date as date_filter\nfrom django.utils.translation import gettext_lazy as _\n\nfrom .registry import field_registry\n\n\nclass ComputationField(object):\n    \"\"\"\n    Computation field responsible for making the calculation unit\n    \"\"\"\n\n    _field_registry = field_registry\n    name = \"\"\n    \"\"\"The name to be used in the ReportGenerator\"\"\"\n\n    calculation_field = \"value\"\n    \"\"\"the Field to compute on\"\"\"\n\n    calculation_method = Sum\n    \"\"\"The computation Method\"\"\"\n\n    verbose_name = None\n    \"\"\"Verbose name to be used in front end when needed\"\"\"\n\n    requires = None\n    \"\"\"This can be a list of sibling classes,\n    they will be asked to compute and their value would be available to you in the `resolve` method\n    requires = [BasicCalculationA, BasicCalculationB]\n    \"\"\"\n\n    type = \"number\"\n    \"\"\"Just a string describing what this computation field return, usually passed to frontend\"\"\"\n\n    is_summable = True\n    \"\"\"Indicate if this computation can be summed over. Useful to be passed to frontend or whenever needed\"\"\"\n\n    report_model = None\n    \"\"\"The model on which the computation would occur\"\"\"\n\n    queryset = None\n    \"\"\"The queryset on which the computation would occur\"\"\"\n\n    group_by = None\n    group_by_custom_querysets = None\n    plus_side_q = None\n    minus_side_q = None\n\n    base_kwargs_filters = None\n    base_q_filters = None\n\n    _require_classes = None\n    _debit_and_credit = True\n\n    prevent_group_by = False\n    \"\"\"Will prevent group by calculation for this specific field, serves when you want to compute overall results\"\"\"\n\n    def __new__(cls, *args, **kwargs):\n        \"\"\"\n        This is where we register the class in the registry\n        :param args:\n        :param kwargs:\n        :return:\n        \"\"\"\n        if not cls.name:\n            raise ValueError(f\"ReportField {cls} must have a name\")\n        return super(ComputationField, cls).__new__(cls)\n\n    @classmethod\n    def create(cls, method, field, name=None, verbose_name=None, is_summable=True):\n        \"\"\"\n        Creates a ReportField class on the fly\n        :param method: The computation Method to be used\n        :param field: The field on which the computation would occur\n        :param name: a name to refer to this field else where\n        :param verbose_name: Verbose name\n        :param is_summable:\n        :return:\n        \"\"\"\n        if not name:\n            name = name or f\"{method.name.lower()}__{field}\"\n            assert name not in cls._field_registry.get_all_report_fields_names()\n\n        verbose_name = verbose_name or f\"{method.name} {field}\"\n        report_klass = type(\n            f\"ReportField_{name}\",\n            (cls,),\n            {\n                \"name\": name,\n                \"verbose_name\": verbose_name,\n                \"calculation_field\": field,\n                \"calculation_method\": method,\n                \"is_summable\": is_summable,\n            },\n        )\n        return report_klass\n\n    def __init__(\n        self,\n        plus_side_q=None,\n        minus_side_q=None,\n        report_model=None,\n        queryset=None,\n        calculation_field=None,\n        calculation_method=None,\n        date_field=\"\",\n        group_by=None,\n        group_by_custom_querysets=None,\n    ):\n        super(ComputationField, self).__init__()\n        self.date_field = date_field\n        self.report_model = self.report_model or report_model\n        self.queryset = self.queryset or queryset\n        self.queryset = self.report_model._default_manager.all() if self.queryset is None else self.queryset\n\n        self.group_by_custom_querysets = self.group_by_custom_querysets or group_by_custom_querysets\n\n        self.calculation_field = calculation_field if calculation_field else self.calculation_field\n        self.calculation_method = calculation_method if calculation_method else self.calculation_method\n        self.plus_side_q = self.plus_side_q or plus_side_q\n        self.minus_side_q = self.minus_side_q or minus_side_q\n        self.requires = self.requires or []\n        self.group_by = self.group_by or group_by\n        self._cache = None, None, None\n        self._require_classes = self._get_required_classes()\n        self._required_prepared_results = None\n\n        self._debit_and_credit = self.plus_side_q or self.minus_side_q\n\n    @classmethod\n    def _get_required_classes(cls):\n        requires = cls.requires or []\n        return [field_registry.get_field_by_name(x) if isinstance(x, str) else x for x in requires]\n\n    def apply_aggregation(self, queryset, group_by=\"\"):\n        annotation = self.calculation_method(self.calculation_field)\n        if self.group_by_custom_querysets:\n            return queryset.aggregate(annotation)\n        elif group_by:\n            queryset = queryset.values(group_by).annotate(annotation)\n            queryset = {str(x[self.group_by]): x for x in queryset}\n        else:\n            queryset = queryset.aggregate(annotation)\n        return queryset\n\n    def init_preparation(self, q_filters=None, kwargs_filters=None, **kwargs):\n        \"\"\"\n        Called by the generator to prepare the calculation of this field + it's requirements\n        :param q_filters:\n        :param kwargs_filters:\n        :param kwargs:\n        :return:\n        \"\"\"\n        kwargs_filters = kwargs_filters or {}\n\n        required_prepared_results = self._prepare_required_computations(q_filters, kwargs_filters.copy())\n        queryset = self.get_queryset()\n        if self.group_by_custom_querysets:\n            debit_results, credit_results = self.prepare_custom_group_by_queryset(q_filters, kwargs_filters, **kwargs)\n        else:\n            debit_results, credit_results = self.prepare(\n                q_filters,\n                kwargs_filters,\n                queryset,\n                self.group_by,\n                self.prevent_group_by,\n                **kwargs,\n            )\n        self._cache = debit_results, credit_results\n        self._required_prepared_results = required_prepared_results\n\n    def prepare_custom_group_by_queryset(self, q_filters=None, kwargs_filters=None, **kwargs):\n        debit_output, credit_output = [], []\n        for index, queryset in enumerate(self.group_by_custom_querysets):\n            debit, credit = self.prepare(q_filters, kwargs_filters, queryset, **kwargs)\n            if debit:\n                debit_output.append(debit)\n            if credit:\n                credit_output.append(credit)\n        return debit_output, credit_output\n\n    def prepare(\n        self,\n        q_filters: list | object = None,\n        kwargs_filters: dict = None,\n        main_queryset=None,\n        group_by: str = None,\n        prevent_group_by=None,\n        **kwargs,\n    ):\n        \"\"\"\n        This is the first hook where you can customize the calculation away from the Django Query aggregation method\n        This method is called with all available arguments, so you can prepare the results for the whole set and save\n        it in a local cache (like self._cache) .\n        The flow will later call the method `resolve`,  giving you the id, for you to return it respective calculation\n\n        :param q_filters:\n        :param kwargs_filters:\n        :param main_queryset:\n        :param group_by:\n        :param prevent_group_by:\n        :param kwargs:\n        :return:\n        \"\"\"\n\n        queryset = main_queryset.all()\n        group_by = \"\" if prevent_group_by else group_by\n        credit_results = None\n\n        if q_filters:\n            if type(q_filters) is Q:\n                q_filters = [q_filters]\n            queryset = queryset.filter(*q_filters)\n        if kwargs_filters:\n            queryset = queryset.filter(**kwargs_filters)\n\n        if self.plus_side_q:\n            queryset = queryset.filter(*self.plus_side_q)\n        debit_results = self.apply_aggregation(queryset, group_by)\n\n        if self._debit_and_credit:\n            queryset = main_queryset.all()\n            if kwargs_filters:\n                queryset = queryset.filter(**kwargs_filters)\n            if q_filters:\n                queryset = queryset.filter(*q_filters)\n            if self.minus_side_q:\n                queryset = queryset.filter(*self.minus_side_q)\n            credit_results = self.apply_aggregation(queryset, group_by)\n\n        return debit_results, credit_results\n\n    def get_queryset(self):\n        queryset = self.queryset\n        if self.base_q_filters:\n            queryset = queryset.filter(*self.base_q_filters)\n        if self.base_kwargs_filters:\n            queryset = queryset.filter(**self.base_kwargs_filters)\n        return queryset.order_by()\n\n    def _prepare_required_computations(\n        self,\n        q_filters=None,\n        extra_filters=None,\n    ):\n        values = {}\n        for required_klass in self._require_classes:\n            dep = required_klass(\n                self.plus_side_q,\n                self.minus_side_q,\n                self.report_model,\n                date_field=self.date_field,\n                group_by=self.group_by,\n                queryset=self.queryset,\n                group_by_custom_querysets=self.group_by_custom_querysets,\n            )\n            results = dep.init_preparation(q_filters, extra_filters)\n            values[dep.name] = {\"results\": results, \"instance\": dep}\n        return values\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        \"\"\"\n        Reponsible for getting the exact data from the prepared value\n        :param prepared_results: the returned data from prepare\n        :param required_computation_results: the returned data from prepare\n        :param current_pk: he value of group by id\n        :param current_row: the row in iteration\n        :return: a solid number or value\n        \"\"\"\n        debit_value, credit_value = self.extract_data(prepared_results, current_pk)\n        value = debit_value or 0 - credit_value or 0\n        return value\n\n    def do_resolve(self, current_obj, current_row=None):\n        prepared_result = self._cache\n        dependencies_value = self._resolve_dependencies(current_obj)\n        return self.resolve(prepared_result, dependencies_value, current_obj, current_row)\n\n    def get_dependency_value(self, current_obj, name):\n        \"\"\"\n        Get the values of the ReportFields specified in `requires`\n\n        :param current_obj: the current object which we want the calculation for\n        :param name: the name of the specific dependency you want.\n\n        :return: a dict containing dependencies names as keys and their calculation as values\n                 or a specific value if name is specified.\n        \"\"\"\n        values = self._resolve_dependencies(current_obj, name=name)\n        return values.get(name)\n\n    def _resolve_dependencies(self, current_obj, name=None):\n        dep_results = {}\n        dependencies_value = self._required_prepared_results\n        dependencies_value = dependencies_value or {}\n        needed_values = [name] if name else dependencies_value.keys()\n        for d in needed_values:\n            d_instance = dependencies_value[d][\"instance\"]\n            dep_results[d] = d_instance.do_resolve(current_obj)\n        return dep_results\n\n    def extract_data(self, prepared_results, current_obj):\n        group_by = \"\" if self.prevent_group_by else (self.group_by or self.group_by_custom_querysets)\n        annotation = \"__\".join([self.calculation_field.lower(), self.calculation_method.name.lower()])\n\n        cached_debit, cached_credit = prepared_results\n\n        cached = [cached_debit, cached_credit]\n        output = []\n        for results in cached:\n            value = 0\n            if results:\n                if not group_by:\n                    x = list(results.keys())[0]\n                    value = results[x]\n                elif self.group_by_custom_querysets:\n                    value = results[int(current_obj)][annotation]\n                else:\n                    value = results.get(str(current_obj), {}).get(annotation, 0)\n            output.append(value)\n        return output\n\n    @classmethod\n    def get_full_dependency_list(cls):\n        \"\"\"\n        Get the full Hirearchy of dependencies and dependencies dependency.\n        :return: List of dependecies classes\n        \"\"\"\n\n        def get_dependency(field_class):\n            dependencies = field_class._get_required_classes()\n            klasses = []\n            for klass in dependencies:\n                klasses.append(klass)\n                other = get_dependency(klass)\n                if other:\n                    klasses += other\n            return klasses\n\n        return get_dependency(cls)\n\n    @classmethod\n    def get_crosstab_field_verbose_name(cls, model, id):\n        \"\"\"\n        Construct a verbose name for the crosstab field\n        :param model: the model name\n        :param id: the id of the current crosstab object\n        :return: a verbose string\n        \"\"\"\n        if id == \"----\":\n            return _(\"The remainder\")\n        return f\"{cls.verbose_name} {model} {id}\"\n\n    @classmethod\n    def get_time_series_field_verbose_name(cls, date_period, index, dates, pattern):\n        \"\"\"\n        Get the name of the verbose name of a computation field that's in a time_series.\n        should be a mix of the date period of the column and it's verbose name.\n        :param date_period: a tuple of (start_date, end_date)\n        :param index: the index of the current field in the whole dates to be calculated\n        :param dates a list of tuples representing the start and the end date\n        :param pattern it's the pattern name. monthly, daily, custom, ...\n        :return: a verbose string\n        \"\"\"\n        dt_format = \"%Y/%m/%d\"\n\n        if pattern == \"monthly\":\n            month_name = date_filter(date_period[0], \"F Y\")\n            return f\"{cls.verbose_name} {month_name}\"\n        elif pattern == \"daily\":\n            return f\"{cls.verbose_name} {date_period[0].strftime(dt_format)}\"\n        elif pattern == \"weekly\":\n            return f' {cls.verbose_name} {_(\"Week\")} {index + 1} {date_period[0].strftime(dt_format)}'\n        elif pattern == \"yearly\":\n            year = date_filter(date_period[0], \"Y\")\n            return f\"{cls.verbose_name} {year}\"\n\n        return f\"{cls.verbose_name} {date_period[0].strftime(dt_format)} - {date_period[1].strftime(dt_format)}\"\n\n\nclass FirstBalanceField(ComputationField):\n    name = \"__fb__\"\n    verbose_name = _(\"opening balance\")\n\n    def prepare(\n        self,\n        q_filters: list | object = None,\n        kwargs_filters: dict = None,\n        main_queryset=None,\n        group_by: str = None,\n        prevent_group_by=None,\n        **kwargs,\n    ):\n        extra_filters = kwargs_filters or {}\n        if self.date_field:\n            from_date_value = extra_filters.get(f\"{self.date_field}__gte\")\n            extra_filters.pop(f\"{self.date_field}__gte\", None)\n            extra_filters[f\"{self.date_field}__lt\"] = from_date_value\n        return super(FirstBalanceField, self).prepare(\n            q_filters, kwargs_filters, main_queryset, group_by, prevent_group_by, **kwargs\n        )\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        if not self.date_field:\n            return 0\n        return super().resolve(prepared_results, required_computation_results, current_pk, current_row)\n\n\nfield_registry.register(FirstBalanceField)\n\n\nclass TotalReportField(ComputationField):\n    name = \"__total__\"\n    verbose_name = _(\"Sum of value\")\n    requires = [\"__debit__\", \"__credit__\"]\n\n\nfield_registry.register(TotalReportField)\n\n\nclass BalanceReportField(ComputationField):\n    name = \"__balance__\"\n    verbose_name = _(\"Closing Total\")\n    requires = [\"__fb__\"]\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        result = super().resolve(prepared_results, required_computation_results, current_pk, current_row)\n        fb = required_computation_results.get(\"__fb__\") or 0\n\n        return result + fb\n\n\nfield_registry.register(BalanceReportField)\n\n\nclass PercentageToTotalBalance(ComputationField):\n    requires = [BalanceReportField]\n    name = \"__percent_to_total_balance__\"\n    verbose_name = _(\"%\")\n\n    prevent_group_by = True\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        result = super().resolve(prepared_results, required_computation_results, current_pk, current_row)\n        return required_computation_results.get(\"__balance__\") / result * 100\n\n\nclass CreditReportField(ComputationField):\n    name = \"__credit__\"\n    verbose_name = _(\"Credit\")\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        debit_value, credit_value = self.extract_data(prepared_results, current_pk)\n        return credit_value\n\n\nfield_registry.register(CreditReportField)\n\n\n@field_registry.register\nclass DebitReportField(ComputationField):\n    name = \"__debit__\"\n    verbose_name = _(\"Debit\")\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        debit_value, credit_value = self.extract_data(prepared_results, current_pk)\n        return debit_value\n\n\n@field_registry.register\nclass CreditQuantityReportField(ComputationField):\n    name = \"__credit_quantity__\"\n    verbose_name = _(\"Credit QTY\")\n    calculation_field = \"quantity\"\n    is_summable = False\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        debit_value, credit_value = self.extract_data(prepared_results, current_pk)\n        return credit_value\n\n\n@field_registry.register\nclass DebitQuantityReportField(ComputationField):\n    name = \"__debit_quantity__\"\n    calculation_field = \"quantity\"\n    verbose_name = _(\"Debit QTY\")\n    is_summable = False\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        debit_value, credit_value = self.extract_data(prepared_results, current_pk)\n        return debit_value\n\n\nclass TotalQTYReportField(ComputationField):\n    name = \"__total_quantity__\"\n    verbose_name = _(\"Total QTY\")\n    calculation_field = \"quantity\"\n    is_summable = False\n\n\nfield_registry.register(TotalQTYReportField)\n\n\nclass FirstBalanceQTYReportField(FirstBalanceField):\n    name = \"__fb_quantity__\"\n    verbose_name = _(\"Opening QTY\")\n    calculation_field = \"quantity\"\n    is_summable = False\n\n\nfield_registry.register(FirstBalanceQTYReportField)\n\n\nclass BalanceQTYReportField(ComputationField):\n    name = \"__balance_quantity__\"\n    verbose_name = _(\"Closing QTY\")\n    calculation_field = \"quantity\"\n    requires = [\"__fb_quantity__\"]\n    is_summable = False\n\n    def resolve(self, prepared_results, required_computation_results: dict, current_pk, current_row=None) -> float:\n        result = super().resolve(prepared_results, required_computation_results, current_pk, current_row)\n        fb = required_computation_results.get(\"__fb_quantity__\") or 0\n        return result + fb\n\n\nfield_registry.register(BalanceQTYReportField)\n\n\nclass SlickReportField(ComputationField):\n    @staticmethod\n    def warn():\n        warn(\n            \"SlickReportField name is deprecated, please use ComputationField instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n\n    @classmethod\n    def create(cls, method, field, name=None, verbose_name=None, is_summable=True):\n        cls.warn()\n        return super().create(method, field, name, verbose_name, is_summable)\n\n    def __new__(cls, *args, **kwargs):\n        cls.warn()\n        return super().__new__(cls, *args, **kwargs)\n"
  },
  {
    "path": "slick_reporting/form_factory.py",
    "content": "import warnings\n\n# warn deprecated\nwarnings.warn(\n    \"slick_reporting.form_factory is deprecated. Use slick_reporting.forms instead\",\n    Warning,\n    stacklevel=2,\n)\n\nfrom .forms import *  # noqa\n"
  },
  {
    "path": "slick_reporting/forms.py",
    "content": "from collections import OrderedDict\n\nfrom crispy_forms.helper import FormHelper\nfrom django import forms\nfrom django.utils.functional import cached_property\nfrom django.utils.translation import gettext_lazy as _\n\nfrom . import app_settings\nfrom .helpers import get_foreign_keys, get_field_from_query_text\n\nTIME_SERIES_CHOICES = (\n    (\"monthly\", _(\"Monthly\")),\n    (\"weekly\", _(\"Weekly\")),\n    (\"annually\", _(\"Yearly\")),\n    (\"daily\", _(\"Daily\")),\n)\n\n\ndef default_formfield_callback(f, **kwargs):\n    kwargs[\"required\"] = False\n    kwargs[\"help_text\"] = \"\"\n    return f.formfield(**kwargs)\n\n\ndef get_crispy_helper(\n    foreign_keys_map=None,\n    crosstab_model=None,\n    crosstab_key_name=None,\n    crosstab_display_compute_remainder=False,\n    add_date_range=True,\n):\n    from crispy_forms.helper import FormHelper\n    from crispy_forms.layout import Column, Layout, Div, Row, Field\n\n    foreign_keys_map = foreign_keys_map or []\n    helper = FormHelper()\n    helper.form_class = \"form-horizontal\"\n    helper.label_class = \"col-sm-2 col-md-2 col-lg-2\"\n    helper.field_class = \"col-sm-10 col-md-10 col-lg-10\"\n    helper.form_tag = False\n    helper.disable_csrf = True\n    helper.render_unmentioned_fields = True\n\n    helper.layout = Layout()\n    if add_date_range:\n        helper.layout.fields.append(\n            Row(\n                Column(Field(\"start_date\"), css_class=\"col-sm-6\"),\n                Column(Field(\"end_date\"), css_class=\"col-sm-6\"),\n                css_class=\"raReportDateRange\",\n            ),\n        )\n    filters_container = Div(css_class=\"mt-20\", style=\"margin-top:20px\")\n    # first add the crosstab model and its display reimder then the rest of the fields\n    if crosstab_model:\n        filters_container.append(Field(crosstab_key_name))\n        if crosstab_display_compute_remainder:\n            filters_container.append(Field(\"crosstab_compute_remainder\"))\n\n    for k in foreign_keys_map:\n        if k != crosstab_key_name:\n            filters_container.append(Field(k))\n    helper.layout.fields.append(filters_container)\n\n    return helper\n\n\ndef get_choices_form_queryset_list(qs):\n    choices = []\n    for row in qs:\n        choices.append((row, row))\n    return choices\n\n\nclass OrderByForm(forms.Form):\n    order_by = forms.CharField(required=False)\n\n    def get_order_by(self, default_field=None):\n        \"\"\"\n        Get the order by specified by teh form or the default field if provided\n        :param default_field:\n        :return: tuple of field and direction\n        \"\"\"\n        if self.is_valid():\n            order_field = self.cleaned_data[\"order_by\"]\n            order_field = order_field or default_field\n            if order_field:\n                return self.parse_order_by_field(order_field)\n        return None, None\n\n    def parse_order_by_field(self, order_field):\n        \"\"\"\n        Specify the field and direction\n        :param order_field: the field to order by\n        :return: tuple of field and direction\n        \"\"\"\n        if order_field:\n            asc = True\n            if order_field[0:1] == \"-\":\n                order_field = order_field[1:]\n                asc = False\n            return order_field, not asc\n        return None, None\n\n\nclass BaseReportForm:\n    def get_filters(self):\n        raise NotImplementedError(\n            \"get_filters() must be implemented in subclass,\"\n            \"should return a tuple of (Q objects, kwargs filter) to be passed to QuerySet.filter()\"\n        )\n\n    def get_start_date(self):\n        raise NotImplementedError(\"get_start_date() must be implemented in subclass,\" \"should return a datetime object\")\n\n    def get_end_date(self):\n        raise NotImplementedError(\"get_end_date() must be implemented in subclass,\" \"should return a datetime object\")\n\n    def get_crosstab_compute_remainder(self):\n        raise NotImplementedError(\n            \"get_crosstab_compute_remainder() must be implemented in subclass,\" \"should return a boolean value\"\n        )\n\n    def get_crosstab_ids(self):\n        raise NotImplementedError(\n            \"get_crosstab_ids() must be implemented in subclass,\" \"should return a list of ids to be used for crosstab\"\n        )\n\n    def get_time_series_pattern(self):\n        raise NotImplementedError(\n            \"get_time_series_pattern() must be implemented in subclass,\"\n            \"should return a string value of a valid time series pattern\"\n        )\n\n    def get_crispy_helper(self):\n        # return a default helper\n        helper = FormHelper()\n        helper.form_class = \"form-horizontal\"\n        helper.label_class = \"col-sm-2 col-md-2 col-lg-2\"\n        helper.field_class = \"col-sm-10 col-md-10 col-lg-10\"\n        helper.form_tag = False\n        helper.disable_csrf = True\n        helper.render_unmentioned_fields = True\n        return helper\n\n\nclass SlickReportForm(BaseReportForm):\n    \"\"\"\n    Holds basic function\n    \"\"\"\n\n    def get_start_date(self):\n        return self.cleaned_data.get(\"start_date\")\n\n    def get_end_date(self):\n        return self.cleaned_data.get(\"end_date\")\n\n    def get_time_series_pattern(self):\n        return self.cleaned_data.get(\"time_series_pattern\")\n\n    def get_filters(self):\n        \"\"\"\n        Get the foreign key filters for report queryset, excluding crosstab ids, handled by `get_crosstab_ids()`\n        :return: a dicttionary of filters to be used with QuerySet.filter(**returned_value)\n        \"\"\"\n        _values = {}\n        if self.is_valid():\n            fk_keys = getattr(self, \"foreign_keys\", [])\n            if fk_keys:\n                fk_keys = fk_keys.items()\n            for key, field in fk_keys:\n                if key in self.cleaned_data and not key == self.crosstab_key_name:\n                    val = self.cleaned_data[key]\n                    if val:\n                        val = [x for x in val.values_list(\"pk\", flat=True)]\n                        _values[\"%s__in\" % key] = val\n            return None, _values\n\n    @cached_property\n    def crosstab_key_name(self):\n        # todo get the model more accurately\n        \"\"\"\n        return the actual foreignkey field name by simply adding an '_id' at the end.\n        This is hook is to customize this naieve approach.\n        :return: key: a string that should be in self.cleaned_data\n        \"\"\"\n        if self.crosstab_field_klass:\n            return self.crosstab_field_klass.attname\n        return f\"{self.crosstab_model}_id\"\n\n    def get_crosstab_ids(self):\n        \"\"\"\n        Get the crosstab ids so they can be sent to the report generator.\n        :return:\n        \"\"\"\n        if self.crosstab_field_klass:\n            if self.crosstab_field_klass.is_relation:\n                qs = self.cleaned_data.get(self.crosstab_key_name)\n                return [x for x in qs.values_list(self.crosstab_field_related_name, flat=True)]\n            else:\n                return self.cleaned_data.get(self.crosstab_key_name)\n        return []\n\n    def get_crosstab_compute_remainder(self):\n        return self.cleaned_data.get(\"crosstab_compute_remainder\", True)\n\n    def get_crispy_helper(self, foreign_keys_map=None, crosstab_model=None, **kwargs):\n        return get_crispy_helper(\n            self.foreign_keys,\n            crosstab_model=getattr(self, \"crosstab_model\", None),\n            crosstab_key_name=getattr(self, \"crosstab_key_name\", None),\n            crosstab_display_compute_remainder=getattr(self, \"crosstab_display_compute_remainder\", False),\n            add_date_range=self.add_start_date or self.add_end_date,\n            **kwargs,\n        )\n\n\ndef _default_foreign_key_widget(f_field):\n    return {\n        \"form_class\": forms.ModelMultipleChoiceField,\n        \"required\": False,\n    }\n\n\ndef report_form_factory(\n    model,\n    crosstab_model=None,\n    display_compute_remainder=True,\n    fkeys_filter_func=None,\n    foreign_key_widget_func=None,\n    excluded_fields=None,\n    initial=None,\n    required=None,\n    show_time_series_selector=False,\n    time_series_selector_choices=None,\n    time_series_selector_default=\"\",\n    time_series_selector_label=None,\n    time_series_selector_allow_empty=False,\n    add_start_date=True,\n    add_end_date=True,\n):\n    \"\"\"\n    Create a Report Form based on the report_model passed by\n    1. adding a start_date and end_date fields\n    2. extract all ForeignKeys on the report_model\n\n    :param model: the report_model\n    :param crosstab_model: crosstab model if any\n    :param display_compute_remainder:  relevant only if crosstab_model is specified. Control if we show the check to\n    display the rest.\n    :param fkeys_filter_func:  a receives an OrderedDict of Foreign Keys names and their model field instances found on\n           the model, return the OrderedDict that would be used\n    :param foreign_key_widget_func: receives a Field class return the used widget like this\n           {'form_class': forms.ModelMultipleChoiceField, 'required': False, }\n    :param excluded_fields: a list of fields to be excluded from the report form\n    :param initial a dict for fields initial\n    :param required a list of fields that should be marked as required\n    :return:\n    \"\"\"\n    crosstab_field_related_name = \"\"\n    crosstab_field_klass = None\n    foreign_key_widget_func = foreign_key_widget_func or _default_foreign_key_widget\n    fkeys_filter_func = fkeys_filter_func or (lambda x: x)\n\n    # gather foreign keys\n    initial = initial or {}\n    required = required or []\n    fkeys_map = get_foreign_keys(model)\n    excluded_fields = excluded_fields or []\n    for excluded in excluded_fields:\n        del fkeys_map[excluded]\n\n    fkeys_map = fkeys_filter_func(fkeys_map)\n\n    fkeys_list = []\n    fields = OrderedDict()\n    if add_start_date:\n        fields[\"start_date\"] = forms.DateTimeField(\n            required=False,\n            label=_(\"From date\"),\n            initial=initial.get(\"start_date\", \"\") or app_settings.SLICK_REPORTING_SETTINGS[\"DEFAULT_START_DATE_TIME\"],\n            widget=forms.DateTimeInput(attrs={\"autocomplete\": \"off\"}),\n        )\n    if add_end_date:\n        fields[\"end_date\"] = forms.DateTimeField(\n            required=False,\n            label=_(\"To  date\"),\n            initial=initial.get(\"end_date\", \"\") or app_settings.SLICK_REPORTING_SETTINGS[\"DEFAULT_END_DATE_TIME\"],\n            widget=forms.DateTimeInput(attrs={\"autocomplete\": \"off\"}),\n        )\n\n    if show_time_series_selector:\n        time_series_choices = list(TIME_SERIES_CHOICES)\n        if time_series_selector_allow_empty:\n            time_series_choices.insert(0, (\"\", \"---------\"))\n\n        fields[\"time_series_pattern\"] = forms.ChoiceField(\n            required=False,\n            initial=time_series_selector_default,\n            label=time_series_selector_label or _(\"Period Pattern\"),\n            choices=time_series_selector_choices or TIME_SERIES_CHOICES,\n        )\n\n    for name, f_field in fkeys_map.items():\n        fkeys_list.append(name)\n        field_attrs = foreign_key_widget_func(f_field)\n        if name in required:\n            field_attrs[\"required\"] = True\n        field_attrs[\"initial\"] = initial.get(name, \"\")\n        fields[name] = f_field.formfield(**field_attrs)\n\n    if crosstab_model:\n        # todo Enhance, add tests , cover cases\n        # Crosstab is a foreign key on model\n        # crosstab is a Char field on model\n        # crosstab is a traversing fk field\n        # crosstab is a traversing Char / choice field\n\n        if display_compute_remainder:\n            fields[\"crosstab_compute_remainder\"] = forms.BooleanField(\n                required=False, label=_(\"Display the crosstab remainder\"), initial=True\n            )\n\n        crosstab_field_klass = get_field_from_query_text(crosstab_model, model)\n        if crosstab_field_klass.is_relation:\n            crosstab_field_related_name = crosstab_field_klass.to_fields[0]\n        else:\n            crosstab_field_related_name = crosstab_field_klass.name\n\n        if \"__\" in crosstab_model:  # traversing field, it won't be added naturally to the form\n            if crosstab_field_klass.is_relation:\n                pass\n            else:\n                fields[crosstab_field_related_name] = forms.MultipleChoiceField(\n                    choices=get_choices_form_queryset_list(\n                        list(\n                            crosstab_field_klass.model.objects.values_list(\n                                crosstab_field_related_name, flat=True\n                            ).distinct()\n                        )\n                    ),\n                    required=False,\n                    label=crosstab_field_klass.verbose_name,\n                )\n\n    bases = (\n        SlickReportForm,\n        forms.BaseForm,\n    )\n    new_form = type(\n        \"ReportForm\",\n        bases,\n        {\n            \"base_fields\": fields,\n            \"_fkeys\": fkeys_list,\n            \"foreign_keys\": fkeys_map,\n            \"crosstab_model\": crosstab_model,\n            \"crosstab_display_compute_remainder\": display_compute_remainder,\n            \"crosstab_field_related_name\": crosstab_field_related_name,\n            \"crosstab_field_klass\": crosstab_field_klass,\n            \"add_start_date\": add_start_date,\n            \"add_end_date\": add_end_date,\n        },\n    )\n    return new_form\n"
  },
  {
    "path": "slick_reporting/generator.py",
    "content": "import datetime\nimport logging\nfrom dataclasses import dataclass\nfrom inspect import isclass\n\nfrom django.core.exceptions import ImproperlyConfigured, FieldDoesNotExist\nfrom django.db.models import Q, ForeignKey\n\nfrom .app_settings import SLICK_REPORTING_DEFAULT_CHARTS_ENGINE\nfrom .fields import ComputationField\nfrom .helpers import get_field_from_query_text\nfrom .registry import field_registry\nfrom . import app_settings\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass Chart:\n    title: str\n    type: str\n    data_source: list\n    title_source: list\n    plot_total: bool = False\n    stacking: bool = False  # only for highcharts\n    engine: str = \"\"\n    entryPoint: str = \"\"\n    COLUMN = \"column\"\n    LINE = \"line\"\n    PIE = \"pie\"\n    BAR = \"bar\"\n    AREA = \"area\"\n\n    def to_dict(self):\n        return dict(\n            title=self.title,\n            type=self.type,\n            data_source=self.data_source,\n            title_source=self.title_source,\n            plot_total=self.plot_total,\n            engine=self.engine,\n            entryPoint=self.entryPoint,\n            stacking=self.stacking,\n        )\n\n\nclass ReportGeneratorAPI:\n    report_model = None\n    \"\"\"The main model where data is \"\"\"\n\n    table_name = \"\"\n    \"\"\"If set, a dynamic model will be created from this database table name\"\"\"\n\n    queryset = None\n    \"\"\"If set, the report will use this queryset instead of the report_model\"\"\"\n\n    \"\"\"\n    Class to generate a Json Object containing report data.\n    \"\"\"\n    date_field = \"\"\n    \"\"\"Main date field to use whenever date filter is needed\"\"\"\n\n    start_date_field_name = None\n    \"\"\"If set, the report will use this field to filter the start date, default to date_field\"\"\"\n\n    end_date_field_name = None\n    \"\"\"If set, the report will use this field to filter the end date, default to date_field\"\"\"\n\n    print_flag = None\n\n    group_by = None\n    \"\"\"The field to use for grouping, if not set then the report is expected to be a sub version of the report model\"\"\"\n\n    group_by_custom_querysets = None\n    \"\"\"A List of querysets representing different group by options\"\"\"\n    group_by_custom_querysets_column_verbose_name = None\n\n    columns = None\n    \"\"\"A list of column names.\n    Columns names can be \n\n    1. A Computation Field\n\n    2. If group_by is set, then any field on the group_by model\n\n    3. If group_by is not set, then any field name on the report_model / queryset\n\n    4. A callable on the generator\n\n    5. Special __time_series__, and __crosstab__ \n       Those can be use to control the position of the time series inside the columns, defaults it's appended at the end\n\n       Example:\n       columns = ['product_id', '__time_series__', 'col_b']\n       Same is true with __crosstab__ \n\n    You can customize aspects of the column by adding it as a tuple like this \n        ('field_name', dict(verbose_name=_('My Enhanced Verbose_name'))\n\n\n     \"\"\"\n\n    time_series_pattern = \"\"\n    \"\"\"\n    If set the Report will compute a time series.\n\n    Possible options are: daily, weekly, semimonthly, monthly, quarterly, semiannually, annually and custom.\n\n    if `custom` is set, you'd need to override  `get_custom_time_series_dates`\n    \"\"\"\n    time_series_columns = None\n    \"\"\"\n    a list of Calculation Field names which will be included in the series calculation.\n     Example: ['__total__', '__total_quantity__'] with compute those 2 fields for all the series\n\n    \"\"\"\n\n    time_series_custom_dates = None\n    \"\"\"\n    Used with `time_series_pattern` set to 'custom'\n    It's a list of tuple, each tuple represent start date & end date\n    Example: [ (start_date_1, end_date_1), (start_date_2, end_date_2), ....]\n    \"\"\"\n\n    crosstab_model = None  # deprecated\n\n    crosstab_field = None\n    \"\"\"\n    If set, a cross tab over this model selected ids (via `crosstab_ids`)  \n    \"\"\"\n\n    crosstab_columns = None\n    \"\"\"The computation fields which will be computed for each crosstab-ed ids \"\"\"\n\n    crosstab_ids = None\n    \"\"\"A list is the ids to create a crosstab report on\"\"\"\n\n    crosstab_ids_custom_filters = None\n\n    crosstab_compute_remainder = True\n    \"\"\"Include an an extra crosstab_columns for the outer group ( ie: all expects those `crosstab_ids`) \"\"\"\n\n    limit_records = None\n    \"\"\"Serves are a main limit to  the returned data of the report_model.\n    Can be beneficial if the results may be huge.\n    \"\"\"\n    swap_sign = False\n\n    crosstab_precomputed = False\n    \"\"\"If True, crosstab reads pre-computed values from the database instead of aggregating raw data.\n    In this mode, crosstab_columns should be a list of DB column name strings (not ComputationField classes).\"\"\"\n\n\nclass ReportGenerator(ReportGeneratorAPI, object):\n    \"\"\"\n    The main class responsible generating the report and managing the flow\n    \"\"\"\n\n    field_registry_class = field_registry\n    \"\"\"You can have a custom computation field locator! It only needs a `get_field_by_name(string)` \n    and returns a ReportField`\"\"\"\n\n    def __init__(\n        self,\n        report_model=None,\n        main_queryset=None,\n        start_date=None,\n        end_date=None,\n        date_field=None,\n        q_filters=None,\n        kwargs_filters=None,\n        group_by=None,\n        group_by_custom_querysets=None,\n        group_by_custom_querysets_column_verbose_name=None,\n        columns=None,\n        time_series_pattern=None,\n        time_series_columns=None,\n        time_series_custom_dates=None,\n        crosstab_field=None,\n        crosstab_columns=None,\n        crosstab_ids=None,\n        crosstab_ids_custom_filters=None,\n        crosstab_compute_remainder=None,\n        crosstab_precomputed=None,\n        swap_sign=False,\n        show_empty_records=None,\n        print_flag=False,\n        doc_type_plus_list=None,\n        doc_type_minus_list=None,\n        limit_records=False,\n        format_row_func=None,\n        container_class=None,\n        start_date_field_name=None,\n        end_date_field_name=None,\n        table_name=None,\n    ):\n        \"\"\"\n\n        :param report_model: Main model containing the data\n        :param main_queryset: Default to report_model.objects\n        :param start_date:\n        :param end_date:\n        :param date_field:\n        :param q_filters:\n        :param kwargs_filters:\n        :param group_by:\n        :param columns:\n        :param time_series_pattern:\n        :param time_series_columns:\n        :param crosstab_model:\n        :param crosstab_columns:\n        :param crosstab_ids:\n        :param crosstab_compute_remainder:\n        :param swap_sign:\n        :param show_empty_records:\n        :param base_model:\n        :param print_flag:\n        :param doc_type_plus_list:\n        :param doc_type_minus_list:\n        :param limit_records:\n        \"\"\"\n        from .app_settings import (\n            SLICK_REPORTING_DEFAULT_START_DATE,\n            SLICK_REPORTING_DEFAULT_END_DATE,\n        )\n\n        super().__init__()\n\n        _table_name = table_name or self.table_name\n        if _table_name and not (report_model or self.report_model):\n            from .dynamic_model import get_dynamic_model\n\n            report_model = get_dynamic_model(_table_name)\n\n        self.report_model = self.report_model or report_model\n        if self.queryset is None:\n            self.queryset = main_queryset\n\n        if not self.report_model and self.queryset is None:\n            raise ImproperlyConfigured(\"report_model or queryset must be set on a class level or via init\")\n\n        main_queryset = self.report_model.objects if self.queryset is None else self.queryset\n\n        self.start_date = start_date or datetime.datetime.combine(\n            SLICK_REPORTING_DEFAULT_START_DATE.date(),\n            SLICK_REPORTING_DEFAULT_START_DATE.time(),\n        )\n\n        self.end_date = end_date or datetime.datetime.combine(\n            SLICK_REPORTING_DEFAULT_END_DATE.date(),\n            SLICK_REPORTING_DEFAULT_END_DATE.time(),\n        )\n        self.date_field = self.date_field or date_field\n\n        self.start_date_field_name = self.start_date_field_name or start_date_field_name or self.date_field\n        self.end_date_field_name = self.end_date_field_name or end_date_field_name or self.date_field\n\n        self.q_filters = q_filters or []\n        self.kwargs_filters = kwargs_filters or {}\n        self.crosstab_field = self.crosstab_field or crosstab_field\n\n        self.crosstab_columns = crosstab_columns or self.crosstab_columns or []\n        self.crosstab_ids = self.crosstab_ids or crosstab_ids or []\n        self.crosstab_ids_custom_filters = self.crosstab_ids_custom_filters or crosstab_ids_custom_filters or []\n\n        self.crosstab_compute_remainder = (\n            self.crosstab_compute_remainder if crosstab_compute_remainder is None else crosstab_compute_remainder\n        )\n        self.crosstab_precomputed = self.crosstab_precomputed if crosstab_precomputed is None else crosstab_precomputed\n        self._precomputed_crosstab_data = {}\n\n        if self.crosstab_precomputed:\n            if not self.crosstab_field:\n                raise ImproperlyConfigured(\"crosstab_precomputed requires crosstab_field to be set\")\n            if not self.crosstab_columns:\n                raise ImproperlyConfigured(\"crosstab_precomputed requires crosstab_columns to be set\")\n\n        self.format_row = format_row_func or self._default_format_row\n\n        main_queryset = self.report_model.objects if main_queryset is None else main_queryset\n        # todo revise & move somewhere nicer, List Report need to override the resetting of order\n        main_queryset = self._remove_order(main_queryset)\n\n        self.columns = columns or self.columns or []\n        self.group_by = group_by or self.group_by\n\n        self.group_by_custom_querysets = group_by_custom_querysets or self.group_by_custom_querysets or []\n\n        self.group_by_custom_querysets_column_verbose_name = (\n            group_by_custom_querysets_column_verbose_name or self.group_by_custom_querysets_column_verbose_name or \"\"\n        )\n        self.time_series_pattern = self.time_series_pattern or time_series_pattern\n        self.time_series_columns = self.time_series_columns or time_series_columns\n        self.time_series_custom_dates = self.time_series_custom_dates or time_series_custom_dates\n        self.container_class = container_class\n\n        if (\n            not (self.date_field or (self.start_date_field_name and self.end_date_field_name))\n            and self.time_series_pattern\n        ):\n            raise ImproperlyConfigured(\n                f\"date_field or [start_date_field_name and end_date_field_name] must \"\n                f\"be set for {container_class or self}\"\n            )\n\n        self._prepared_results = {}\n        self.report_fields_classes = {}\n\n        self._report_fields_dependencies = {\n            \"time_series\": {},\n            \"crosstab\": {},\n            \"normal\": {},\n        }\n        self.existing_dependencies = {\"series\": [], \"matrix\": [], \"normal\": []}\n\n        self.print_flag = print_flag or self.print_flag\n\n        # todo validate columns is not empty (if no time series / cross tab)\n\n        if self.group_by:\n            try:\n                self.group_by_field = get_field_from_query_text(self.group_by, self.report_model)\n\n            except (IndexError, AttributeError):\n                raise ImproperlyConfigured(\n                    f\"Can not find group_by field:{self.group_by} in report_model {self.report_model} \"\n                )\n            if \"__\" not in self.group_by:\n                self.group_by_field_attname = self.group_by_field.attname\n            else:\n                self.group_by_field_attname = self.group_by\n\n        else:\n            self.group_by_field_attname = None\n\n        # doc_types = form.get_doc_type_plus_minus_lists()\n        doc_types = [], []\n        self.doc_type_plus_list = list(doc_type_plus_list) if doc_type_plus_list else doc_types[0]\n        self.doc_type_minus_list = list(doc_type_minus_list) if doc_type_minus_list else doc_types[1]\n\n        self.swap_sign = self.swap_sign or swap_sign\n        self.limit_records = self.limit_records or limit_records\n\n        # todo delete this\n        self.show_empty_records = False  # show_empty_records if show_empty_records else self.show_empty_records\n\n        # Preparing actions\n        self._parse()\n\n        self.main_queryset = self.prepare_queryset(main_queryset)\n        self._prepare_report_dependencies()\n\n    def _get_fk_group_by_queryset(self, filtered_qs):\n        \"\"\"Return the related-model queryset for a ForeignKey group_by field.\"\"\"\n        ids = filtered_qs.values_list(self.group_by_field_attname).distinct()\n        # uses the same logic that is in Django's query.py when fields is empty in values() call\n        concrete_fields = [f.name for f in self.group_by_field.related_model._meta.concrete_fields]\n        # add database columns that are not already in concrete_fields\n        final_fields = concrete_fields + list(set(self.get_database_columns()) - set(concrete_fields))\n        return self.group_by_field.related_model.objects.filter(\n            **{f\"{self.group_by_field.target_field.name}__in\": ids}\n        ).values(*final_fields)\n\n    def prepare_queryset(self, queryset):\n        if self.crosstab_precomputed:\n            self._build_precomputed_crosstab_data(queryset)\n            self._crosstab_parsed_columns = self.get_crosstab_parsed_columns()\n\n            if not self.group_by:\n                return [{}]\n            filtered_qs = self._apply_queryset_options(queryset)\n            if type(self.group_by_field) is ForeignKey:\n                return self._get_fk_group_by_queryset(filtered_qs)\n            return filtered_qs.values(self.group_by_field_attname).distinct()\n\n        if self.group_by_custom_querysets:\n            return [{\"__index__\": i} for i, v in enumerate(self.group_by_custom_querysets)]\n        elif self.group_by:\n            main_queryset = self._apply_queryset_options(queryset)\n            if type(self.group_by_field) is ForeignKey:\n                return self._get_fk_group_by_queryset(main_queryset)\n            else:\n                return main_queryset.distinct().values(self.group_by_field_attname)\n\n        return [{}]\n\n    def _remove_order(self, main_queryset):\n        \"\"\"\n        Remove order_by from the main queryset\n        :param main_queryset:\n        :return:\n        \"\"\"\n        # if main_queryset.query.order_by:\n        main_queryset = main_queryset.order_by()\n        return main_queryset\n\n    def _apply_queryset_options(self, query, fields=None):\n        \"\"\"\n        Apply the filters to the main queryset which will computed results be mapped to\n        :param query:\n        :param fields:\n        :return:\n        \"\"\"\n        filters = {}\n        if self.date_field:\n            filters = {\n                f\"{self.start_date_field_name}__gt\": self.start_date,\n                f\"{self.end_date_field_name}__lte\": self.end_date,\n            }\n        filters.update(self.kwargs_filters)\n\n        if filters:\n            query = query.filter(**filters)\n        if self.q_filters:\n            query = query.filter(*self.q_filters)\n        if fields:\n            return query.values(*fields)\n        return query.values()\n\n    def _apply_precomputed_queryset_options(self, query):\n        \"\"\"Apply filters for precomputed crosstab data fetch, using __gte/__lte boundaries.\"\"\"\n        filters = {}\n        if self.date_field:\n            filters = {\n                f\"{self.start_date_field_name}__gte\": self.start_date,\n                f\"{self.end_date_field_name}__lte\": self.end_date,\n            }\n        filters.update(self.kwargs_filters)\n        if filters:\n            query = query.filter(**filters)\n        if self.q_filters:\n            query = query.filter(*self.q_filters)\n        return query.values()\n\n    def _build_precomputed_crosstab_data(self, queryset):\n        \"\"\"Pre-fetch all rows and build the precomputed crosstab lookup dict.\"\"\"\n        filtered_qs = self._apply_precomputed_queryset_options(queryset)\n        rows = filtered_qs.values(self.group_by_field_attname, self.crosstab_field, *self.crosstab_columns)\n\n        crosstab_values_set = set()\n        for row in rows:\n            group_key = str(row[self.group_by_field_attname])\n            crosstab_val = str(row[self.crosstab_field])\n            crosstab_values_set.add(row[self.crosstab_field])\n\n            if group_key not in self._precomputed_crosstab_data:\n                self._precomputed_crosstab_data[group_key] = {}\n            self._precomputed_crosstab_data[group_key][crosstab_val] = {col: row[col] for col in self.crosstab_columns}\n\n        if not self.crosstab_ids:\n            self.crosstab_ids = sorted(crosstab_values_set)\n\n    def _construct_crosstab_filter(self, col_data, queryset_filters=None):\n        \"\"\"\n        In charge of adding the needed crosstab filter, specific to the case of is_remainder or not\n        :param col_data:\n        :return:\n        \"\"\"\n        if queryset_filters:\n            return queryset_filters[0], queryset_filters[1]\n\n        if \"__\" in col_data[\"crosstab_field\"]:\n            column_name = col_data[\"crosstab_field\"]\n        else:\n            field = get_field_from_query_text(col_data[\"crosstab_field\"], self.report_model)\n            column_name = field.column\n        if col_data[\"is_remainder\"] and not queryset_filters:\n            filters = [~Q(**{f\"{column_name}__in\": self.crosstab_ids})]\n        else:\n            filters = [Q(**{f\"{column_name}\": col_data[\"id\"]})]\n        return filters, {}\n\n    def _prepare_report_dependencies(self):\n        from .fields import ComputationField\n\n        all_columns = (\n            (\"normal\", self._parsed_columns),\n            (\"time_series\", self._time_series_parsed_columns),\n            (\"crosstab\", self._crosstab_parsed_columns),\n        )\n        for window, window_cols in all_columns:\n            for col_data in window_cols:\n                klass = col_data[\"ref\"]\n\n                if isclass(klass) and issubclass(klass, ComputationField):\n                    dependencies_names = klass.get_full_dependency_list()\n\n                    # check if any of these dependencies is on the report, if found we call the child to\n                    # resolve the value for its parent avoiding extra database call\n                    fields_on_report = [\n                        x\n                        for x in window_cols\n                        if x[\"ref\"] in dependencies_names\n                        and (\n                            (\n                                window == \"time_series\"\n                                and x.get(\"start_date\", \"\") == col_data.get(\"start_date\", \"\")\n                                and x.get(\"end_date\") == col_data.get(\"end_date\")\n                            )\n                            or window == \"crosstab\"\n                            and x.get(\"id\") == col_data.get(\"id\")\n                        )\n                    ]\n                    for field in fields_on_report:\n                        self._report_fields_dependencies[window][field[\"name\"]] = col_data[\"name\"]\n            for col_data in window_cols:\n                klass = col_data[\"ref\"]\n                name = col_data[\"name\"]\n\n                # if column has a dependency then skip it\n                if not (isclass(klass) and issubclass(klass, ComputationField)):\n                    continue\n                if self._report_fields_dependencies[window].get(name, False):\n                    continue\n\n                report_class = klass(\n                    self.doc_type_plus_list,\n                    self.doc_type_minus_list,\n                    group_by=self.group_by,\n                    report_model=self.report_model,\n                    date_field=self.date_field,\n                    queryset=self.queryset,\n                    group_by_custom_querysets=self.group_by_custom_querysets,\n                )\n\n                q_filters = None\n                date_filter = {}\n                if self.start_date_field_name:\n                    date_filter[f\"{self.start_date_field_name}__gte\"] = col_data.get(\"start_date\", self.start_date)\n                if self.end_date_field_name:\n                    date_filter[f\"{self.end_date_field_name}__lt\"] = col_data.get(\"end_date\", self.end_date)\n\n                date_filter.update(self.kwargs_filters)\n                if window == \"crosstab\" or col_data.get(\"computation_flag\", \"\") == \"crosstab\":\n                    q_filters, kw_filters = col_data[\"queryset_filters\"]\n                    date_filter.update(kw_filters)\n\n                report_class.init_preparation(q_filters, date_filter)\n                self.report_fields_classes[name] = report_class\n\n    # @staticmethod\n    def get_primary_key_name(self, model):\n        if self.group_by_custom_querysets:\n            return \"__index__\"\n        for field in model._meta.fields:\n            if field.primary_key:\n                return field.attname\n        return \"\"\n\n    def _get_record_data(self, obj, columns):\n        \"\"\"\n        the function is run for every obj in the main_queryset\n        :param obj: current row\n        :param: columns： The columns we iterate on\n        :return: a dict object containing all needed data\n        \"\"\"\n\n        data = {}\n        group_by_val = None\n        if self.group_by_custom_querysets:\n            group_by_val = str(obj[\"__index__\"])\n\n        elif self.group_by:\n            if self.group_by_field.related_model and \"__\" not in self.group_by:\n                primary_key_name = self.get_primary_key_name(self.group_by_field.related_model)\n            else:\n                primary_key_name = self.group_by_field_attname\n\n            column_data = obj.get(primary_key_name, obj.get(\"id\"))\n            group_by_val = str(column_data)\n\n        for window, window_cols in columns:\n            for col_data in window_cols:\n                name = col_data[\"name\"]\n\n                if col_data.get(\"source\", \"\") == \"precomputed_crosstab\":\n                    crosstab_val = col_data[\"crosstab_value\"]\n                    crosstab_col = col_data[\"crosstab_column\"]\n                    group_data = self._precomputed_crosstab_data.get(group_by_val, {})\n                    data[name] = group_data.get(crosstab_val, {}).get(crosstab_col, 0)\n\n                elif col_data.get(\"source\", \"\") == \"attribute_field\":\n                    data[name] = col_data[\"ref\"](obj, data)\n                elif col_data.get(\"source\", \"\") == \"container_class_attribute_field\":\n                    data[name] = col_data[\"ref\"](obj, data)\n\n                elif (\n                    col_data.get(\"source\", \"\") == \"magic_field\" and (self.group_by or self.group_by_custom_querysets)\n                ) or (not (self.group_by or self.group_by_custom_querysets)):\n                    source = self._report_fields_dependencies[window].get(name, False)\n\n                    if source:\n                        computation_class = self.report_fields_classes[source]\n                        # the computation field is being asked from another computation field that requires it.\n                        value = computation_class.get_dependency_value(group_by_val, col_data[\"ref\"].name)\n                    else:\n                        try:\n                            computation_class = self.report_fields_classes[name]\n                        except KeyError:\n                            continue\n                        value = computation_class.do_resolve(group_by_val, data)\n                    if self.swap_sign:\n                        value = -value\n                    data[name] = value\n\n                else:\n                    data[name] = obj.get(name, \"\")\n        return data\n\n    def get_report_data(self):\n        main_queryset = self.main_queryset[: self.limit_records] if self.limit_records else self.main_queryset\n\n        all_columns = (\n            (\"normal\", self._parsed_columns),\n            (\"time_series\", self._time_series_parsed_columns),\n            (\"crosstab\", self._crosstab_parsed_columns),\n        )\n\n        get_record_data = self._get_record_data\n        format_row = self.format_row\n        data = [format_row(get_record_data(obj, all_columns)) for obj in main_queryset]\n        return data\n\n    def _default_format_row(self, row_obj):\n        \"\"\"\n        Hook where you can format row values like properly format a date\n        :param row_obj:\n        :return:\n        \"\"\"\n        return row_obj\n\n    @staticmethod\n    def check_columns(\n        cls,\n        columns,\n        group_by,\n        report_model,\n        container_class=None,\n        group_by_custom_querysets=None,\n    ):\n        \"\"\"\n        Check and parse the columns, throw errors in case an item in the columns cant not identified\n        :param columns: List of columns\n        :param group_by: group by field if any\n        :param report_model: the report model\n        :param container_class: a class to search for custom columns attribute in, typically the ReportView\n        :param group_by_custom_querysets a list of group by custom queries Or None.\n        :return: List of dict, each dict contains relevant data to the respective field in `columns`\n        \"\"\"\n\n        group_by_model = None\n        if group_by_custom_querysets:\n            if \"__index__\" not in columns:\n                columns.insert(0, \"__index__\")\n\n        if group_by:\n            try:\n                group_by_field = [x for x in report_model._meta.get_fields() if x.name == group_by.split(\"__\")[0]][0]\n            except IndexError:\n                raise ImproperlyConfigured(\n                    f\"ReportView {cls}: Could not find the group_by field: `{group_by}` in \"\n                    f\"report_model: `{report_model}`\"\n                )\n            if group_by_field.is_relation:\n                group_by_model = group_by_field.related_model\n            else:\n                group_by_model = report_model\n\n        parsed_columns = []\n        for col in columns:\n            options = {}\n            if type(col) is tuple:\n                col, options = col\n\n            if col in [\"__time_series__\", \"__crosstab__\"]:\n                #     These are placeholder not real computation field\n                continue\n\n            magic_field_class = None\n            attribute_field = None\n            is_container_class_attribute = False\n\n            if isinstance(col, str):\n                attribute_field = getattr(cls, col, None)\n                if attribute_field is None:\n                    is_container_class_attribute = True\n                    attribute_field = getattr(container_class, col, None)\n\n            elif issubclass(col, ComputationField):\n                magic_field_class = col\n\n            try:\n                magic_field_class = magic_field_class or field_registry.get_field_by_name(col)\n            except KeyError:\n                magic_field_class = None\n\n            if attribute_field:\n                col_data = {\n                    \"name\": col,\n                    \"verbose_name\": getattr(attribute_field, \"verbose_name\", col),\n                    \"source\": \"container_class_attribute_field\" if is_container_class_attribute else \"attribute_field\",\n                    \"ref\": attribute_field,\n                    \"type\": \"text\",\n                }\n            elif magic_field_class:\n                # a magic field\n                col_data = {\n                    \"name\": magic_field_class.name,\n                    \"verbose_name\": magic_field_class.verbose_name,\n                    \"source\": \"magic_field\",\n                    \"ref\": magic_field_class,\n                    \"type\": magic_field_class.type,\n                    \"is_summable\": magic_field_class.is_summable,\n                }\n            else:\n                # A database field\n                if group_by_custom_querysets and col == \"__index__\":\n                    # group by custom queryset special case: which is the index\n                    col_data = {\n                        \"name\": col,\n                        \"verbose_name\": cls.group_by_custom_querysets_column_verbose_name,\n                        \"source\": \"database\",\n                        \"ref\": \"\",\n                        \"type\": \"text\",\n                    }\n                    col_data.update(options)\n                    parsed_columns.append(col_data)\n                    continue\n\n                model_to_use = group_by_model if group_by and \"__\" not in group_by else report_model\n                group_by_str = str(group_by)\n                if \"__\" in group_by_str:\n                    related_model = get_field_from_query_text(group_by, model_to_use).related_model\n                    model_to_use = related_model if related_model else model_to_use\n\n                try:\n                    if \"__\" in col:\n                        # A traversing link order__client__email\n                        field = get_field_from_query_text(col, model_to_use)\n                    else:\n                        field = model_to_use._meta.get_field(col)\n                except FieldDoesNotExist:\n                    field = getattr(container_class, col, False)\n\n                    if not field:\n                        raise FieldDoesNotExist(\n                            f'Field \"{col}\" not found either as an attribute to the generator class {cls}, '\n                            f'{f\"Container class {container_class},\" if container_class else \"\"}'\n                            f'or a computation field, or a database column for the model \"{model_to_use}\"'\n                        )\n\n                col_data = {\n                    \"name\": col,\n                    \"verbose_name\": getattr(field, \"verbose_name\", col),\n                    \"source\": \"database\",\n                    \"ref\": field,\n                    \"type\": \"choice\" if field.choices else field.get_internal_type(),\n                }\n            col_data.update(options)\n            parsed_columns.append(col_data)\n        return parsed_columns\n\n    def _parse(self):\n        self.parsed_columns = self.check_columns(\n            self,\n            self.columns,\n            self.group_by,\n            self.report_model,\n            self.container_class,\n            self.group_by_custom_querysets,\n        )\n        self._parsed_columns = list(self.parsed_columns)\n        self._crosstab_parsed_columns = self.get_crosstab_parsed_columns()\n        self._time_series_parsed_columns = self.get_time_series_parsed_columns()\n\n    def get_database_columns(self):\n        return [col[\"name\"] for col in self.parsed_columns if \"source\" in col and col[\"source\"] == \"database\"]\n\n    # def get_method_columns(self):\n    #     return [col['name'] for col in self.parsed_columns if col['type'] == 'method']\n\n    def get_list_display_columns(self):\n        columns = self.parsed_columns\n        if self.time_series_pattern:\n            time_series_columns = self.get_time_series_parsed_columns()\n            try:\n                index = self.columns.index(\"__time_series__\")\n                columns[index:index] = time_series_columns\n            except ValueError:\n                columns += time_series_columns\n\n        if self.crosstab_field:\n            crosstab_columns = self.get_crosstab_parsed_columns()\n\n            try:\n                index = self.columns.index(\"__crosstab__\")\n                columns[index:index] = crosstab_columns\n            except ValueError:\n                columns += crosstab_columns\n\n        return columns\n\n    def get_time_series_parsed_columns(self):\n        \"\"\"\n        Return time series columns with all needed data attached\n        :param plain: if True it returns '__total__' instead of '__total_TS011212'\n        :return: List if columns\n        \"\"\"\n        _values = []\n\n        cols = self.time_series_columns or []\n        series = self._get_time_series_dates(self.time_series_pattern)\n\n        for index, dt in enumerate(series):\n            for col in cols:\n                magic_field_class = None\n\n                if isinstance(col, str):\n                    magic_field_class = field_registry.get_field_by_name(col)\n                elif issubclass(col, ComputationField):\n                    magic_field_class = col\n\n                _values.append(\n                    {\n                        \"name\": magic_field_class.name + \"TS\" + dt[1].strftime(\"%Y%m%d\"),\n                        \"original_name\": magic_field_class.name,\n                        \"verbose_name\": self.get_time_series_field_verbose_name(magic_field_class, dt, index, series),\n                        \"ref\": magic_field_class,\n                        \"start_date\": dt[0],\n                        \"end_date\": dt[1],\n                        \"source\": \"magic_field\" if magic_field_class else \"\",\n                        \"is_summable\": magic_field_class.is_summable,\n                    }\n                )\n\n            # append the crosstab fields, if they exist, on the time_series\n            if self._crosstab_parsed_columns:\n                for parsed_col in self._crosstab_parsed_columns:\n                    parsed_col = parsed_col.copy()\n                    parsed_col[\"name\"] = parsed_col[\"name\"] + \"TS\" + dt[1].strftime(\"%Y%m%d\")\n                    parsed_col[\"start_date\"] = dt[0]\n                    parsed_col[\"end_date\"] = dt[1]\n                    _values.append(parsed_col)\n\n        return _values\n\n    def get_time_series_field_verbose_name(self, computation_class, date_period, index, series, pattern=None):\n        \"\"\"\n        Sent the column data to construct a verbose name.\n        Default implementation is delegated to the ReportField.get_time_series_field_verbose_name\n        (which is  name + the end date %Y%m%d)\n\n        :param computation_class: the computation field_name\n        :param date_period: a tuple of (start_date, end_date)\n        :return: a verbose string\n        \"\"\"\n        pattern = pattern or self.time_series_pattern\n        return computation_class.get_time_series_field_verbose_name(date_period, index, series, pattern)\n\n    def get_custom_time_series_dates(self):\n        \"\"\"\n        Hook to get custom , maybe separated date periods\n        :return: [ (date1,date2) , (date3,date4), .... ]\n        \"\"\"\n        return self.time_series_custom_dates or []\n\n    def _get_time_series_dates(self, series=None, start_date=None, end_date=None):\n        from dateutil.relativedelta import relativedelta\n\n        series = series or self.time_series_pattern\n        start_date = start_date or self.start_date\n        end_date = end_date or self.end_date\n        _values = []\n\n        if series:\n            if series == \"daily\":\n                time_delta = datetime.timedelta(days=1)\n            elif series == \"weekly\":\n                time_delta = relativedelta(weeks=1)\n            elif series == \"bi-weekly\":\n                time_delta = relativedelta(weeks=2)\n            elif series == \"monthly\":\n                time_delta = relativedelta(months=1)\n            elif series == \"quarterly\":\n                time_delta = relativedelta(months=3)\n            elif series == \"semiannually\":\n                time_delta = relativedelta(months=6)\n            elif series == \"annually\":\n                time_delta = relativedelta(years=1)\n            elif series == \"custom\":\n                return self.get_custom_time_series_dates()\n            else:\n                raise NotImplementedError(f'\"{series}\" is not implemented for time_series_pattern')\n\n            done = False\n\n            while not done:\n                to_date = start_date + time_delta\n                _values.append((start_date, to_date))\n                start_date = to_date\n                if to_date >= end_date:\n                    done = True\n        return _values\n\n    def get_crosstab_parsed_columns(self):\n        \"\"\"\n        Return a list of the columns analyzed , with reference to computation field and everything\n        :return:\n        \"\"\"\n        if self.crosstab_precomputed:\n            return self._get_precomputed_crosstab_parsed_columns()\n\n        report_columns = self.crosstab_columns or []\n\n        ids = list(self.crosstab_ids) or list(self.crosstab_ids_custom_filters)\n        if self.crosstab_compute_remainder and not self.crosstab_ids_custom_filters:\n            ids.append(\"----\")\n        output_cols = []\n\n        ids_length = len(ids) - 1\n        for counter, crosstab_id in enumerate(ids):\n            queryset_filters = None\n\n            if self.crosstab_ids_custom_filters:\n                queryset_filters = crosstab_id\n                crosstab_id = counter\n\n            for col in report_columns:\n                magic_field_class = None\n                if isinstance(col, str):\n                    magic_field_class = field_registry.get_field_by_name(col)\n                elif issubclass(col, ComputationField):\n                    magic_field_class = col\n\n                crosstab_column = {\n                    \"name\": f\"{magic_field_class.name}CT{crosstab_id}\",\n                    \"original_name\": magic_field_class.name,\n                    \"verbose_name\": self.get_crosstab_field_verbose_name(\n                        magic_field_class, self.crosstab_field, crosstab_id\n                    ),\n                    \"ref\": magic_field_class,\n                    \"id\": crosstab_id,\n                    \"crosstab_field\": self.crosstab_field,\n                    \"is_remainder\": counter == ids_length if self.crosstab_compute_remainder else False,\n                    \"source\": \"magic_field\" if magic_field_class else \"\",\n                    \"is_summable\": magic_field_class.is_summable,\n                    \"computation_flag\": \"crosstab\",  # a flag, todo find a better way probably\n                }\n                crosstab_column[\"queryset_filters\"] = self._construct_crosstab_filter(crosstab_column, queryset_filters)\n\n                output_cols.append(crosstab_column)\n\n        return output_cols\n\n    def _get_precomputed_crosstab_parsed_columns(self):\n        \"\"\"Build column metadata for precomputed crosstab from discovered values.\"\"\"\n        columns = []\n        for crosstab_val in self.crosstab_ids:\n            sanitized = _sanitize_crosstab_key(crosstab_val)\n            for col_name in self.crosstab_columns:\n                columns.append(\n                    {\n                        \"name\": f\"{col_name}CT{sanitized}\",\n                        \"original_name\": col_name,\n                        \"verbose_name\": f\"{col_name} {crosstab_val}\",\n                        \"source\": \"precomputed_crosstab\",\n                        \"is_summable\": True,\n                        \"ref\": \"\",\n                        \"crosstab_value\": str(crosstab_val),\n                        \"crosstab_column\": col_name,\n                        \"type\": \"number\",\n                        \"visible\": True,\n                    }\n                )\n        return columns\n\n    def get_crosstab_field_verbose_name(self, computation_class, model, id):\n        \"\"\"\n        Hook to change the crosstab field verbose name, default it delegate this function to the ReportField\n        :param computation_class: ReportField Class\n        :param model: the model name as string\n        :param id: the current crosstab id\n        :return: a verbose string\n        \"\"\"\n        return computation_class.get_crosstab_field_verbose_name(model, id)\n\n    def get_metadata(self):\n        \"\"\"\n        A hook to send data about the report for front end which can later be used in charting\n        :return:\n        \"\"\"\n        time_series_columns = self.get_time_series_parsed_columns()\n        crosstab_columns = self.get_crosstab_parsed_columns()\n        metadata = {\n            \"time_series_pattern\": self.time_series_pattern,\n            \"time_series_column_names\": [x[\"name\"] for x in time_series_columns],\n            \"time_series_column_verbose_names\": [x[\"verbose_name\"] for x in time_series_columns],\n            \"crosstab_model\": self.crosstab_field or \"\",\n            \"crosstab_column_names\": [x[\"name\"] for x in crosstab_columns],\n            \"crosstab_column_verbose_names\": [x[\"verbose_name\"] for x in crosstab_columns],\n        }\n        return metadata\n\n    def get_columns_data(self):\n        \"\"\"\n        Hook to get the columns information to front end\n        :param columns:\n        :return:\n        \"\"\"\n        columns = self.get_list_display_columns()\n        data = []\n\n        for col in columns:\n            data.append(\n                {\n                    \"name\": col[\"name\"],\n                    \"computation_field\": col.get(\"original_name\", \"\"),\n                    \"verbose_name\": col[\"verbose_name\"],\n                    \"visible\": col.get(\"visible\", True),\n                    \"type\": col.get(\"type\", \"text\"),\n                    \"is_summable\": col.get(\"is_summable\", \"\"),\n                }\n            )\n        return data\n\n    def get_full_response(\n        self, data=None, report_slug=None, chart_settings=None, default_chart_title=None, default_chart_engine=None\n    ):\n        data = data or self.get_report_data()\n        data = {\n            \"report_slug\": report_slug or self.__class__.__name__,\n            \"data\": data,\n            \"columns\": self.get_columns_data(),\n            \"metadata\": self.get_metadata(),\n            \"chart_settings\": self.get_chart_settings(\n                chart_settings, default_chart_title=default_chart_title, chart_engine=default_chart_engine\n            ),\n        }\n        return data\n\n    @staticmethod\n    def get_chart_settings(chart_settings=None, default_chart_title=None, chart_engine=None):\n        \"\"\"\n        Ensure the sane settings are passed to the front end. ?\n        \"\"\"\n        chart_engine = chart_engine or SLICK_REPORTING_DEFAULT_CHARTS_ENGINE\n        output = []\n        chart_settings = chart_settings or []\n        report_title = default_chart_title or \"\"\n        for i, chart in enumerate(chart_settings):\n            if type(chart) is Chart:\n                chart = chart.to_dict()\n            chart[\"id\"] = chart.get(\"id\", f\"{i}\")\n            chart[\"engine_name\"] = chart.get(\"engine_name\", chart_engine)\n            chart_type = chart.get(\"type\", \"line\")\n            if chart_type == \"column\" and chart[\"engine_name\"] == \"chartsjs\":\n                chart[\"type\"] = \"bar\"\n            if not chart.get(\"title\", False):\n                chart[\"title\"] = report_title\n            chart[\"entryPoint\"] = (\n                chart.get(\"entryPoint\")\n                or app_settings.SLICK_REPORTING_SETTINGS[\"CHARTS\"][chart[\"engine_name\"]][\"entryPoint\"]\n            )\n            chart[\"stacking\"] = chart.get(\"stacking\", False)\n\n            output.append(chart)\n        return output\n\n\nclass ListViewReportGenerator(ReportGenerator):\n    def prepare_queryset(self, queryset):\n        return self._apply_queryset_options(queryset, self.get_database_columns())\n\n    def _apply_queryset_options(self, query, fields=None):\n        \"\"\"\n        Apply the filters to the main queryset which will computed results be mapped to\n        :param query:\n        :param fields:\n        :return:\n        \"\"\"\n        filters = {}\n        if self.date_field:\n            filters = {\n                f\"{self.date_field}__gt\": self.start_date,\n                f\"{self.date_field}__lte\": self.end_date,\n            }\n        filters.update(self.kwargs_filters)\n\n        if filters:\n            query = query.filter(**filters)\n        if fields:\n            return query.values(*fields)\n        return query\n\n    def _get_record_data(self, obj, columns):\n        \"\"\"\n        the function is run for every obj in the main_queryset\n        :param obj: current row\n        :param: columns： The columns we iterate on\n        :return: a dict object containing all needed data\n        \"\"\"\n\n        data = {}\n        group_by_val = None\n        if self.group_by:\n            if self.group_by_field.related_model and \"__\" not in self.group_by:\n                primary_key_name = self.get_primary_key_name(self.group_by_field.related_model)\n            else:\n                primary_key_name = self.group_by_field_attname\n\n            column_data = obj.get(primary_key_name, obj.get(\"id\"))\n            group_by_val = str(column_data)\n\n        for window, window_cols in columns:\n            for col_data in window_cols:\n                name = col_data[\"name\"]\n\n                if col_data.get(\"source\", \"\") == \"attribute_field\":\n                    data[name] = col_data[\"ref\"](self, obj, data)\n                    # changed line\n                elif col_data.get(\"source\", \"\") == \"container_class_attribute_field\":\n                    data[name] = col_data[\"ref\"](obj)\n\n                elif (col_data.get(\"source\", \"\") == \"magic_field\" and self.group_by) or (\n                    self.time_series_pattern and not self.group_by\n                ):\n                    source = self._report_fields_dependencies[window].get(name, False)\n                    if source:\n                        computation_class = self.report_fields_classes[source]\n                        value = computation_class.get_dependency_value(group_by_val, col_data[\"ref\"].name)\n                    else:\n                        try:\n                            computation_class = self.report_fields_classes[name]\n                        except KeyError:\n                            continue\n                        value = computation_class.do_resolve(group_by_val, data)\n                    if self.swap_sign:\n                        value = -value\n                    data[name] = value\n\n                else:\n                    data[name] = obj[name]\n        return data\n\n    def _remove_order(self, main_queryset):\n        return main_queryset\n\n\ndef _sanitize_crosstab_key(value):\n    \"\"\"Sanitize a crosstab value for use in column names. Replace non-alphanumeric chars with underscores.\"\"\"\n    import re\n\n    return re.sub(r\"[^a-zA-Z0-9_]\", \"_\", str(value))\n"
  },
  {
    "path": "slick_reporting/helpers.py",
    "content": "from collections import OrderedDict\n\nfrom django.conf import settings\nfrom django.contrib.contenttypes.fields import GenericForeignKey\n\n\ndef get_calculation_annotation(calculation_field, calculation_method):\n    \"\"\"\n    Returns the default django annotation\n    @param calculation_field: the field to calculate ex 'value'\n    @param calculation_method: the aggregation method ex: Sum\n    @return: the annotation ex value__sum\n    \"\"\"\n\n    return \"__\".join([calculation_field.lower(), calculation_method.name.lower()])\n\n\ndef get_foreign_keys(model):\n    \"\"\"\n    Scans a model and return an Ordered Dictionary with the foreign keys found\n    :param model: the model to scan\n    :return: Ordered Dict\n    \"\"\"\n    from django.db import models\n\n    fields = model._meta.get_fields()\n    fkeys = OrderedDict()\n    for f in fields:\n        if (\n            f.is_relation\n            and type(f) is not models.OneToOneRel\n            and type(f) is not models.ManyToOneRel\n            and type(f) is not models.ManyToManyRel\n            and type(f) is not GenericForeignKey\n        ):\n            fkeys[f.attname] = f\n    return fkeys\n\n\ndef get_field_from_query_text(path, model):\n    \"\"\"\n    return the field of a query text\n    `modelA__modelB__foo_field` would return foo_field on modelsB\n    :param path:\n    :param model:\n    :return:\n    \"\"\"\n    relations = path.split(\"__\")\n    _rel = model\n    field = None\n    for i, m in enumerate(relations):\n        field = _rel._meta.get_field(m)\n        if i == len(relations) - 1:\n            return field\n        _rel = field.related_model\n    return field\n\n\ndef user_test_function(report_view):\n    \"\"\"\n    A default test function return True on DEBUG, otherwise return the user.is_superuser\n    :param report_view:\n    :return:\n    \"\"\"\n    if not settings.DEBUG:\n        return report_view.request.user.is_superuser\n    return True\n"
  },
  {
    "path": "slick_reporting/locale/ar/LC_MESSAGES/django.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same license as the PACKAGE package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: PACKAGE VERSION\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2026-04-19 11:13+0200\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: LANGUAGE <LL@li.org>\\n\"\n\"Language: \\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 \"\n\"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\\n\"\n#: app_settings.py:88\nmsgid \"Total\"\nmsgstr \"الإجمالي\"\n\n#: app_settings.py:89\nmsgid \"Export to CSV\"\nmsgstr \"تصدير إلى CSV\"\n\n#: fields.py:355\nmsgid \"The remainder\"\nmsgstr \"المتبقي\"\n\n#: fields.py:387\nmsgid \"opening balance\"\nmsgstr \"الرصيد الافتتاحي\"\n\n#: fields.py:418\nmsgid \"Sum of value\"\nmsgstr \"مجموع القيمة\"\n\n#: fields.py:427\nmsgid \"Closing Total\"\nmsgstr \"الإجمالي الختامي\"\n\n#: fields.py:443\nmsgid \"%\"\nmsgstr \"%\"\n\n#: fields.py:454\nmsgid \"Credit\"\nmsgstr \"دائن\"\n\n#: fields.py:467\nmsgid \"Debit\"\nmsgstr \"مدين\"\n\n#: fields.py:477\nmsgid \"Credit QTY\"\nmsgstr \"الكمية الدائنة\"\n\n#: fields.py:490\nmsgid \"Debit QTY\"\nmsgstr \"الكمية المدينة\"\n\n#: fields.py:500\nmsgid \"Total QTY\"\nmsgstr \"إجمالي الكمية\"\n\n#: fields.py:510\nmsgid \"Opening QTY\"\nmsgstr \"الكمية الافتتاحية\"\n\n#: fields.py:520\nmsgid \"Closing QTY\"\nmsgstr \"الكمية الختامية\"\n\n#: forms.py:12\nmsgid \"Monthly\"\nmsgstr \"شهري\"\n\n#: forms.py:13\nmsgid \"Weekly\"\nmsgstr \"أسبوعي\"\n\n#: forms.py:14\nmsgid \"Yearly\"\nmsgstr \"سنوي\"\n\n#: forms.py:15\nmsgid \"Daily\"\nmsgstr \"يومي\"\n\n#: forms.py:280\nmsgid \"From date\"\nmsgstr \"من تاريخ\"\n\n#: forms.py:287\nmsgid \"To  date\"\nmsgstr \"إلى تاريخ\"\n\n#: forms.py:300\nmsgid \"Period Pattern\"\nmsgstr \"نمط الفترة\"\n\n#: forms.py:321\nmsgid \"Display the crosstab remainder\"\nmsgstr \"عرض متبقي الجدول المتقاطع\"\n\n#: templates/slick_reporting/report.html:9\nmsgid \"Filters\"\nmsgstr \"المرشحات\"\n\n#: templates/slick_reporting/report.html:15\nmsgid \"Filter\"\nmsgstr \"تصفية\"\n\n#: templates/slick_reporting/report.html:31\nmsgid \"Results\"\nmsgstr \"النتائج\"\n"
  },
  {
    "path": "slick_reporting/locale/de/LC_MESSAGES/django.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same license as the PACKAGE package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n#\n\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: PACKAGE VERSION\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2026-04-19 11:28+0200\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: LANGUAGE <LL@li.org>\\n\"\n\"Language: \\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n#: app_settings.py:88\nmsgid \"Total\"\nmsgstr \"Gesamt\"\n\n#: app_settings.py:89\nmsgid \"Export to CSV\"\nmsgstr \"Als CSV exportieren\"\n\n#: fields.py:355\nmsgid \"The remainder\"\nmsgstr \"Der Rest\"\n\n#: fields.py:387\nmsgid \"opening balance\"\nmsgstr \"Eröffnungssaldo\"\n\n#: fields.py:418\nmsgid \"Sum of value\"\nmsgstr \"Wertsumme\"\n\n#: fields.py:427\nmsgid \"Closing Total\"\nmsgstr \"Abschlusssumme\"\n\n#: fields.py:443\nmsgid \"%\"\nmsgstr \"%\"\n\n#: fields.py:454\nmsgid \"Credit\"\nmsgstr \"Haben\"\n\n#: fields.py:467\nmsgid \"Debit\"\nmsgstr \"Soll\"\n\n#: fields.py:477\nmsgid \"Credit QTY\"\nmsgstr \"Haben-Menge\"\n\n#: fields.py:490\nmsgid \"Debit QTY\"\nmsgstr \"Soll-Menge\"\n\n#: fields.py:500\nmsgid \"Total QTY\"\nmsgstr \"Gesamtmenge\"\n\n#: fields.py:510\nmsgid \"Opening QTY\"\nmsgstr \"Eröffnungsmenge\"\n\n#: fields.py:520\nmsgid \"Closing QTY\"\nmsgstr \"Abschlussmenge\"\n\n#: forms.py:12\nmsgid \"Monthly\"\nmsgstr \"Monatlich\"\n\n#: forms.py:13\nmsgid \"Weekly\"\nmsgstr \"Wöchentlich\"\n\n#: forms.py:14\nmsgid \"Yearly\"\nmsgstr \"Jährlich\"\n\n#: forms.py:15\nmsgid \"Daily\"\nmsgstr \"Täglich\"\n\n#: forms.py:280\nmsgid \"From date\"\nmsgstr \"Von Datum\"\n\n#: forms.py:287\nmsgid \"To  date\"\nmsgstr \"Bis Datum\"\n\n#: forms.py:300\nmsgid \"Period Pattern\"\nmsgstr \"Periodenmuster\"\n\n#: forms.py:321\nmsgid \"Display the crosstab remainder\"\nmsgstr \"Kreuztabellen-Rest anzeigen\"\n\n#: templates/slick_reporting/report.html:9\nmsgid \"Filters\"\nmsgstr \"Filter\"\n\n#: templates/slick_reporting/report.html:15\nmsgid \"Filter\"\nmsgstr \"Filtern\"\n\n#: templates/slick_reporting/report.html:31\nmsgid \"Results\"\nmsgstr \"Ergebnisse\"\n"
  },
  {
    "path": "slick_reporting/registry.py",
    "content": "from __future__ import unicode_literals\n\nfrom django.contrib.admin.sites import AlreadyRegistered, NotRegistered\n\n\nclass ReportFieldRegistry(object):\n    def __init__(self):\n        super(ReportFieldRegistry, self).__init__()\n        self._registry = {}  # holds\n\n    def register(self, report_field, override=False):\n        \"\"\"\n        Register a report_field into the registry,\n        :param report_field:\n        :param override: if True, a report_field will get replaced if found, else it would throw an AlreadyRegistered\n        :return: report_field passed\n        \"\"\"\n        if report_field.name in self._registry and not override:\n            raise AlreadyRegistered(f\"The field name {report_field.name} is used before and `override` is False\")\n\n        self._registry[report_field.name] = report_field\n        return report_field\n\n    def unregister(self, report_field):\n        \"\"\"\n        To unregister a Report Field\n        :param report_field: a Report field class or a ReportField Name\n        :return: None\n        \"\"\"\n        name = report_field if isinstance(report_field, str) else report_field.name\n        if name not in self._registry:\n            raise NotRegistered(report_field)\n        del self._registry[name]\n\n    def get_field_by_name(self, name):\n        if name in self._registry:\n            return self._registry[name]\n        else:\n            raise KeyError(\n                f'{name} is not found in the report field registry. Options are {\",\".join(self.get_all_report_fields_names())}'\n            )\n\n    def get_all_report_fields_names(self):\n        return list(self._registry.keys())\n\n\nfield_registry = ReportFieldRegistry()\n"
  },
  {
    "path": "slick_reporting/static/slick_reporting/slick_reporting.chartsjs.js",
    "content": "// type / title_source / data_source,\n// title\n\n(function ($) {\n\n\n    var COLORS = ['#7cb5ec', '#f7a35c', '#90ee7e', '#7798BF', '#aaeeee', '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'];\n\n    let _chart_cache = {};\n\n    function is_time_series(response, chartOptions) {\n        if (chartOptions.time_series_support === false) return false;\n        return response['metadata']['time_series_pattern'] !== \"\";\n    }\n\n    function is_crosstab(response, chartOptions) {\n        return response['metadata']['crosstab_model'] || '';\n    }\n\n    function getTimeSeriesColumnNames(response) {\n        return response['metadata']['time_series_column_names'];\n    }\n\n    function createChartObject(response, chartOptions, extraOptions) {\n        let extractedData = extractDataFromResponse(response, chartOptions);\n\n        // Chart.js has no 'area' type; use 'line' with fill\n        let chartType = chartOptions.type;\n        let fillArea = false;\n        if (chartType === 'area') {\n            chartType = 'line';\n            fillArea = true;\n        }\n\n        let chartObject = {\n            type: chartType,\n            'data': {\n                labels: extractedData.labels,\n                datasets: extractedData.datasets,\n            },\n            'options': {\n                'responsive': true,\n                plugins: {\n                    title: {\n                        display: true,\n                        text: chartOptions.title,\n                    },\n                    tooltip: {\n                        mode: 'index',\n                    },\n                },\n            }\n        };\n\n        if (chartOptions.type === 'pie') {\n            chartObject['options'] = {\n                responsive: true,\n                maintainAspectRatio: true,\n                aspectRatio: 2,\n                plugins: {\n                    title: {\n                        display: true,\n                        text: chartOptions.title,\n                    },\n                },\n            }\n        }\n        if (chartOptions.stacking === true) {\n            chartObject['options']['scales'] = {\n                y: {stacked: true},\n                x: {stacked: true},\n            }\n        }\n        if (fillArea) {\n            chartObject.data.datasets.forEach(function (ds) {\n                ds.fill = true;\n            });\n        }\n        return chartObject\n    }\n\n    function getGroupByLabelAndSeries(response, chartOptions) {\n\n        let legendResults = [];\n        let datasetData = [];\n        let dataFieldName = chartOptions['data_source'];\n        let titleFieldName = chartOptions['title_source'];\n\n        for (let i = 0; i < response.data.length; i++) {\n            let row = response.data[i];\n            if (titleFieldName !== '') {\n                let txt = row[titleFieldName];\n                txt = $(txt).text() || txt; // the title is an <a tag , we want the text only\n                legendResults.push(txt)\n            }\n            datasetData.push(parseFloat(row[dataFieldName]))\n        }\n        return {\n            'labels': legendResults,\n            \"series\": datasetData,\n        }\n    }\n\n    function getCrosstabColumnNames(response, chartOptions) {\n        let colNames = [];\n        let dataFieldName = chartOptions['data_source'];\n        if (typeof dataFieldName === 'string') dataFieldName = [dataFieldName];\n        dataFieldName.forEach(function (source) {\n            response.columns.forEach(function (col) {\n                if (col.computation_field === source) {\n                    colNames.push(col.name);\n                }\n            });\n        });\n        return colNames;\n    }\n\n    function extractDataFromResponse(response, chartOptions) {\n        let dataFieldName = chartOptions['data_source'];\n        let titleFieldName = chartOptions['title_source'];\n        let isTimeSeries = is_time_series(response, chartOptions);\n        let isCrosstab = is_crosstab(response, chartOptions);\n        let datasets = [];\n        let legendResults = [];\n        let datasetData = [];\n\n        if (isTimeSeries) {\n            legendResults = response.metadata['time_series_column_verbose_names'];\n            let seriesColNames = getTimeSeriesColumnNames(response);\n\n            // Pie charts on time series should always show totals\n            if (chartOptions.type === 'pie') {\n                chartOptions.plot_total = true;\n            }\n\n            if (chartOptions.plot_total) {\n                let results = $.slick_reporting.calculateTotalOnObjectArray(response.data, seriesColNames);\n                for (let fieldIdx = 0; fieldIdx < seriesColNames.length; fieldIdx++) {\n                    datasetData.push(results[seriesColNames[fieldIdx]])\n                }\n                datasets.push({\n                    label: chartOptions.title,\n                    data: datasetData,\n                    backgroundColor: getBackgroundColors(),\n                    borderColor: getBackgroundColors(),\n                    fill: chartOptions.stacking === true,\n                })\n\n\n            } else {\n\n                for (let i = 0; i < response.data.length; i++) {\n                    let row = response.data[i];\n                    let rowData = [];\n                    for (let field = 0; field < seriesColNames.length; field++) {\n                        rowData.push(response.data[i][seriesColNames[field]])\n                    }\n                    let txt = row[titleFieldName];\n                    try {\n                        txt = $($.parseHTML(txt)).text() || txt;\n                    } catch (e) {\n                        // title is not HTML, use as-is\n                    }\n                    datasets.push({\n                        label: txt,\n                        data: rowData,\n                        backgroundColor: getBackgroundColors(i),\n                        borderColor: getBackgroundColors(i),\n                        fill: chartOptions.stacking === true,\n                    })\n                }\n            }\n\n            return {\n                'labels': legendResults,\n                'datasets': datasets,\n            }\n        }\n\n        if (isCrosstab) {\n            legendResults = response.metadata['crosstab_column_verbose_names'];\n            let crosstabColNames = getCrosstabColumnNames(response, chartOptions);\n\n            if (chartOptions.plot_total) {\n                let results = $.slick_reporting.calculateTotalOnObjectArray(response.data, crosstabColNames);\n                for (let fieldIdx = 0; fieldIdx < crosstabColNames.length; fieldIdx++) {\n                    datasetData.push(results[crosstabColNames[fieldIdx]])\n                }\n                datasets.push({\n                    label: chartOptions.title,\n                    data: datasetData,\n                    backgroundColor: getBackgroundColors(),\n                    borderColor: getBackgroundColors(),\n                })\n            } else {\n                for (let i = 0; i < response.data.length; i++) {\n                    let row = response.data[i];\n                    let rowData = [];\n                    for (let field = 0; field < crosstabColNames.length; field++) {\n                        rowData.push(row[crosstabColNames[field]])\n                    }\n                    let txt = row[titleFieldName];\n                    try {\n                        txt = $($.parseHTML(txt)).text() || txt;\n                    } catch (e) {}\n                    datasets.push({\n                        label: txt,\n                        data: rowData,\n                        backgroundColor: getBackgroundColors(i),\n                        borderColor: getBackgroundColors(i),\n                    })\n                }\n            }\n            return {\n                'labels': legendResults,\n                'datasets': datasets,\n            }\n        }\n\n        let results = getGroupByLabelAndSeries(response, chartOptions);\n        datasets = [{\n            data: results.series,\n            backgroundColor: getBackgroundColors(),\n            label: chartOptions.title\n        }];\n        return {\n            'labels': results.labels,\n            'datasets': datasets,\n        }\n    }\n\n    function getBackgroundColors(i) {\n        if (typeof (i) !== 'undefined') {\n            return COLORS[i]\n        }\n        return COLORS\n    }\n\n    function displayChart(data, $elem, chartOptions) {\n        // chart_id = chart_id || $elem.attr('data-report-default-chart') || '';\n        if ($elem.find('canvas').length === 0) {\n            $elem.append(\"<canvas width=\\\"400\\\" height=\\\"100\\\"></canvas>\");\n        }\n\n        let cache_key = $.slick_reporting.get_xpath($elem) + \":\" + data.report_slug + ':' + chartOptions.id;\n        try {\n            let existing_chart = _chart_cache[cache_key];\n            if (typeof (existing_chart) !== 'undefined') {\n                existing_chart.destroy();\n            }\n        } catch (e) {\n            console.error(e)\n        }\n\n        let chartObject = $.slick_reporting.chartsjs.createChartObject(data, chartOptions);\n        let canvas = $elem.find('canvas')[0];\n        try {\n            _chart_cache[cache_key] = new Chart(canvas, chartObject);\n        } catch (e) {\n            console.error(e);\n            $elem.find('canvas').remove();\n        }\n\n\n    }\n\n\n    if (typeof ($.slick_reporting) === 'undefined') {\n        $.slick_reporting = {}\n    }\n    $.slick_reporting.chartsjs = {\n        getGroupByLabelAndSeries: getGroupByLabelAndSeries,\n        createChartObject: createChartObject,\n        displayChart: displayChart,\n        defaults: {\n            // normalStackedTooltipFormatter: normalStackedTooltipFormatter,\n            messages: {\n                noData: 'No Data to display ... :-/',\n                total: 'Total',\n                percent: 'Percent',\n            },\n            credits: {\n                // text: 'RaSystems.io',\n                // href: 'https://rasystems.io'\n            },\n            notify_error: function () {\n            },\n            enable3d: false,\n\n        }\n    };\n\n}(jQuery));\n"
  },
  {
    "path": "slick_reporting/static/slick_reporting/slick_reporting.datatable.js",
    "content": "/**\n * Created by ramez on 2/5/15.\n * A wrapper around Datatables.net\n *\n */\n\n\n(function ($) {\n    let _cache = {};\n    let _instances = {}\n\n\n    function constructTable(css_class, cols, cols_names, add_footer, total_verbose, total_fields, data) {\n        // Construct an HTML table , header and footer , without a body as it is filled by th datatable.net plugin\n        cols = typeof cols != 'undefined' ? cols : false;\n        cols_names = typeof cols_names != 'undefined' ? cols_names : cols;\n\n        let return_val = `<table class=\"${css_class}\"> <thead><tr>`;\n        let header_th = '';\n        let footer_th = '';\n        let footer_colspan = 0;\n        let stop_colspan_detection = false;\n        let totals_container = $.slick_reporting.calculateTotalOnObjectArray(data, total_fields);\n        if (data.length <= 1) {\n            add_footer = false;\n        }\n\n        for (let i = 0; i < cols.length; i++) {\n            let col_name = cols[i].name;\n            header_th += `<th data-id=\"${col_name}\">${cols_names[i]}</th>`;\n            if (total_fields.indexOf(col_name) !== -1) {\n                stop_colspan_detection = true;\n            }\n            if (!stop_colspan_detection) {\n                footer_colspan += 1;\n            } else {\n                let column_total = totals_container[col_name]\n                if (!(column_total || column_total === 0)) {\n                    column_total = ''\n                }\n                footer_th += `<th data-id=${col_name}\">${column_total}</th>`;\n            }\n        }\n        let footer = '';\n        if (add_footer && stop_colspan_detection) {\n            footer = '<tfoot><tr class=\"tr-totals active\"><th colspan=\"' + footer_colspan + '\" style=\"text-align:left\">' + total_verbose + '</th>' + footer_th + '</tr></tfoot>';\n        }\n        return_val = return_val + header_th + `</tr> </thead>${footer}</table>`;\n        return return_val;\n    }\n\n\n    function buildAndInitializeDataTable(data, $elem, extraOptions, successFunction) {\n        // Responsible for turning a ReportView Response into a datatable.\n\n        let opts = $.extend({}, $.slick_reporting.datatable.defaults, extraOptions);\n        opts['datatableContainer'] = $elem;\n\n        let datatable_container = opts.datatableContainer;\n\n        let provide_total = true; // typeof provide_total == 'undefined' ? true : provide_total;\n        let total_fields = []; //# frontend_settings.total_fields || [];\n        let column_names = [];\n        for (let i = 0; i < data['columns'].length; i++) {\n            let col = data['columns'][i];\n            column_names.push(col['verbose_name']);\n            if (col['is_summable'] === true) {\n                total_fields.push(col['name'])\n            }\n        }\n\n        if (total_fields.length === 0) provide_total = false;\n\n        datatable_container.html(constructTable(\n            $.slick_reporting.datatable.defaults.tableCssClass, data['columns'], column_names,\n            provide_total, opts.messages.total, total_fields, data.data));\n        initializeReportDatatable(datatable_container.find('table'), data, opts);\n\n        if (typeof (successFunction) === 'function') {\n            successFunction(data);\n        }\n\n    }\n\n\n    function getDatatableColumns(data) {\n        let columns = [];\n        for (let i = 0; i < data['columns'].length; i++) {\n\n            let server_data = data['columns'][i];\n            let col_data = {\n                \"data\": server_data['name'],\n                'visible': server_data['visible'],\n                'title': server_data['verbose_name']\n            };\n            columns.push(col_data);\n\n        }\n        return columns;\n    }\n\n\n    function initializeReportDatatable(tableSelector, data, extraOptions) {\n        tableSelector = typeof tableSelector != 'undefined' ? tableSelector : '.datatable';\n        extraOptions = typeof extraOptions != 'undefined' ? extraOptions : {};\n\n        let opts = $.extend({}, $.slick_reporting.datatable.defaults, extraOptions);\n\n\n        let dom = typeof (extraOptions.dom) == 'undefined' ? 'lfrtip' : extraOptions.dom;\n        let paging = typeof (extraOptions.paging) == 'undefined' ? true : extraOptions.paging;\n        let ordering = typeof (extraOptions.ordering) == 'undefined' ? true : extraOptions.ordering;\n        let info = typeof (extraOptions.info) == 'undefined' ? true : extraOptions.info;\n        let searching = typeof (extraOptions.searching) == 'undefined' ? true : extraOptions.searching;\n        if (data.data.length === 0) dom = '<\"mb-20\"t>';\n\n        let datatableOptions = $.extend({}, extraOptions['datatableOptions']);\n\n        datatableOptions.dom = dom;\n        datatableOptions.ordering = ordering;\n        datatableOptions.paging = paging;\n        datatableOptions.info = info;\n        datatableOptions.searching = searching;\n\n        datatableOptions.sorting = [];\n        datatableOptions.processing = true;\n        datatableOptions.data = data['data'];\n        datatableOptions.columns = getDatatableColumns(data);\n        datatableOptions.initComplete = function (settings, json) {\n            setTimeout(function () {\n                if (opts.enableFixedHeader) {\n                    new $.fn.dataTable.FixedHeader(dt, {\"zTop\": \"2001\"});\n                }\n            }, 100);\n\n        };\n        _instances[data.report_slug] = $(tableSelector).DataTable(datatableOptions);\n    }\n\n\n    $.slick_reporting.datatable = {\n        initializeDataTable: initializeReportDatatable,\n        _cache: _cache,\n        buildAdnInitializeDatatable: buildAndInitializeDataTable,\n        constructTable: constructTable,\n        instances: _instances\n    }\n}(jQuery));\n\n$.slick_reporting.datatable.defaults = {\n\n    enableFixedHeader: false,\n    fixedHeaderZindex: 2001,\n    messages: {\n        total: $.slick_reporting.defaults.total_label,\n    },\n    tableCssClass: 'table table-xxs datatable-basic table-bordered table-striped table-hover ',\n\n    datatableOptions: { // datatables options sent to its constructor.\n        css_class: 'display'\n\n    }\n};"
  },
  {
    "path": "slick_reporting/static/slick_reporting/slick_reporting.highchart.js",
    "content": "/**\n * Created by Ramez on 11/20/14.\n * Updated to support modern Highcharts API (v11+).\n */\n(function ($) {\n\n        function dataArrayToObject(data, key) {\n            // Turn a data array to an object\n            // Example:\n            // in: [\n            // {'key': key , 'value': 0},\n            // {'key': key , 'value': 1},\n            // {'key': key , 'value': 2},\n            // ]\n            // out: {'key':key , 'value':[0,1,2]}\n            var output = {};\n            for (var r = 0; r < data.length; r++) {\n                output[data[r][key]] = data[r];\n            }\n            return output\n        }\n\n\n        let _chart_cache = {};\n\n        function normalStackedTooltipFormatter() {\n\n            var tooltip = '<small>' + this.x + '</small><table>' +\n                '<tr><td style=\"color: ' + this.series.color + '\">' + this.series.name + ': </td> <td style=\"text-align: right\"><b>' + this.point.y + ' </b></td></tr>' +\n                '<tr><td style=\"color: ' + this.series.color + '\">{pertoTotal}:</td><td style=\"text-align: right\"><b>' + this.point.percentage.toFixed(2) + ' %</b></td></tr>' +\n                '<tr><td> {Total}:</td><td style=\"text-align: right\"><b>' + this.point.stackTotal + '<b></td></tr>' +\n                '</table>';\n//style=\"color: '+ this.series.color+'\"\n            tooltip = tooltip.format($.slick_reporting.highcharts.defaults.messages);\n            return tooltip\n\n        }\n\n        function transform_to_pie(chartObject_series, index, categories) {\n            index = index || 0;\n            let new_series_data = []\n            chartObject_series.forEach(function (elem, key) {\n                new_series_data.push({\n                    'name': elem.name,\n                    'y': elem.data[index]\n                })\n            })\n            return {\n                'name': categories[index],\n                'data': new_series_data\n            }\n        }\n\n        function createChartObject(response, chartOptions, extraOptions) {\n            // Create the chart Object\n            // First specifying the global defaults then apply the specification from the response\n\n            try {\n                $.extend(chartOptions, {\n                    'sub_title': '',\n                });\n                chartOptions.data = response.data;\n\n                let is_time_series = is_timeseries_support(response, chartOptions); // response.metadata.time_series_pattern || '';\n                let is_crosstab = is_crosstab_support(response, chartOptions);\n\n                let chart_type = chartOptions.type;\n                let enable3d = false;\n                let chart_data = {};\n\n                let rtl = false; // $.slick_reporting.highcharts.defaults.rtl;\n\n                if (is_time_series) {\n                    chart_data = get_time_series_data(response, chartOptions)\n                } else if (is_crosstab) {\n                    chart_data = get_crosstab_data(response, chartOptions)\n                } else {\n                    chart_data = get_normal_data(response, chartOptions)\n                }\n\n\n                let highchart_object = {\n                    chart: {\n                        type: '',\n                    },\n                    title: {\n                        text: chartOptions.title,\n                    },\n                    subtitle: {\n                        text: chartOptions.sub_title,\n                        useHTML: true\n                    },\n                    yAxis: {\n                        opposite: rtl,\n                    },\n                    xAxis: {\n                        labels: {enabled: true},\n                        reversed: rtl,\n                    },\n                    tooltip: {\n                        useHTML: true\n                    },\n                    plotOptions: {},\n                    exporting: {\n                        allowHTML: true,\n\n                        enabled: true,\n                    }\n                };\n\n\n                highchart_object.series = chart_data.series;\n                if (chart_type === 'bar' || chart_type === 'column' || chart_type === 'pie') {\n\n                    highchart_object.chart.type = chart_type;\n\n                    if (chart_type === 'bar' || chart_type === 'column') {\n                        highchart_object['xAxis'] = {\n                            categories: chart_data['titles'],\n                        };\n                    }\n                    highchart_object['yAxis']['labels'] = {overflow: 'justify'};\n                }\n\n                if (chart_type === 'pie') {\n                    highchart_object.series = [transform_to_pie(chart_data.series, 0, chart_data.categories)]\n                    highchart_object.plotOptions = {\n                        pie: {\n                            allowPointSelect: true,\n                            cursor: 'pointer',\n                            dataLabels: {\n                                enabled: true,\n                                format: '{point.percentage:.1f}% <b>{point.name}</b>',\n                            },\n                            showInLegend: false,\n                        }\n                    };\n\n                    highchart_object['legend'] = {\n                        layout: 'vertical',\n                        align: 'right',\n                        verticalAlign: 'top',\n                        x: -40,\n                        y: 100,\n                        floating: true,\n                        borderWidth: 1,\n\n                        shadow: true\n                    };\n\n                    if (enable3d) {\n                        highchart_object.chart.options3d = {\n                            enabled: true,\n                            alpha: 45,\n                            beta: 0\n                        };\n                        highchart_object.plotOptions.pie.innerSize = 100;\n                        highchart_object.plotOptions.pie.depth = 45;\n                    }\n                } else if (chart_type === 'column') {\n                    if (enable3d) {\n                        highchart_object.chart.options3d = {\n                            enabled: true,\n                            alpha: 10,\n                            beta: 25,\n                            depth: 50\n                        };\n                        highchart_object.plotOptions.column = {\n                            depth: 25\n                        };\n                        highchart_object.chart.margin = 70;\n                    }\n                    if (chartOptions['stacking']) {\n                        let stackingValue = chartOptions['stacking'] === true ? 'normal' : chartOptions['stacking'];\n                        highchart_object.plotOptions.series = {stacking: stackingValue};\n                    }\n                    if (chartOptions['tooltip_formatter']) {\n                        highchart_object.tooltip = {\n                            useHTML: true,\n                            formatter: chartOptions['tooltip_formatter'],\n                            valueDecimals: 2\n                        }\n                    }\n\n\n                } else if (chart_type === 'area') {\n                    highchart_object.chart.type = 'area';\n\n                    let areaStacking = chartOptions['stacking'] === true ? 'normal' : (chartOptions['stacking'] || undefined);\n                    highchart_object.plotOptions = {\n                        area: {\n                            stacking: areaStacking,\n                            marker: {\n                                enabled: false\n                            }\n                        }\n                    }\n                } else if (chart_type === 'line') {\n                    let marker_enabled = true;\n                    highchart_object.chart.type = 'line';\n                    // disable marker when ticks are more then 12 , relying on the hover of the mouse ;\n                    try {\n                        if (highchart_object.series[0].data.length > 12) marker_enabled = false;\n                    } catch (err) {\n\n                    }\n\n                    highchart_object.plotOptions = {\n                        line: {\n                            marker: {\n                                enabled: false\n                            }\n                        }\n                    };\n                    highchart_object.xAxis.labels.enabled = marker_enabled;\n\n                    highchart_object.tooltip.useHTML = true;\n                    highchart_object.tooltip.shared = true;\n                    highchart_object.tooltip.crosshairs = true;\n                }\n\n                if (is_time_series) {\n                    if (chartOptions.plot_total && chartOptions.type !== 'line') {\n                        highchart_object.xAxis.categories = [chartOptions.title]\n                    } else {\n                        highchart_object.xAxis.categories = chart_data.titles;\n                    }\n                    highchart_object.xAxis.tickmarkPlacement = 'on';\n                    if (chart_type !== 'line')\n                        highchart_object.tooltip.shared = false //Option here;\n                } else {\n                    highchart_object.xAxis.categories = chart_data.titles\n                }\n\n                highchart_object.credits = $.slick_reporting.highcharts.defaults.credits;\n                highchart_object.lang = {\n                    noData: $.slick_reporting.highcharts.defaults.messages.noData\n                };\n                return highchart_object;\n            } catch (err) {\n                console.log(err);\n            }\n        }\n\n        function get_normal_data(response, chartOptions) {\n            let data_sources = {};\n            let series = []\n            chartOptions.data_source;\n            let categories = []\n            chartOptions.data_source.forEach(function (elem, key) {\n                data_sources[elem] = []; //{'name': chartOptions.series_name[key],}\n                response.columns.forEach(function (col, key) {\n                    if (elem === col.name) {\n                        data_sources[elem].push(col.name)\n                        categories.push(col.verbose_name)\n                    }\n                })\n            })\n\n            response.data.forEach(function (elem, index) {\n                series.push({\n                    'name': elem[chartOptions.title_source],\n                    'data': [elem[chartOptions.data_source]]\n                })\n            })\n            return {\n                'categories': categories,\n                'titles': categories,\n                'series': series,\n            }\n        }\n\n        function get_time_series_data(response, chartOptions) {\n\n            let series = []\n            let data_sources = {};\n            chartOptions.data_source.forEach(function (elem, key) {\n                data_sources[elem] = [];\n                response.columns.forEach(function (col, key) {\n                    if (col.computation_field === elem) {\n                        data_sources[elem].push(col.name)\n                    }\n                })\n            })\n            if (!chartOptions.plot_total) {\n                response.data.forEach(function (elem, index) {\n                    Object.keys(data_sources).forEach(function (series_cols, index) {\n                        let data = []\n                        data_sources[series_cols].forEach(function (col, index) {\n                            data.push(elem[col])\n                        })\n                        series.push({\n                            'name': elem[chartOptions.title_source],\n                            'data': data\n                        })\n                    })\n                })\n            } else {\n                let all_column_to_be_summed = []\n                let data = []\n                Object.keys(data_sources).forEach(function (series_cols, index) {\n                    all_column_to_be_summed = all_column_to_be_summed.concat(data_sources[series_cols]);\n                })\n                let totalValues = $.slick_reporting.calculateTotalOnObjectArray(response.data, all_column_to_be_summed)\n\n                Object.keys(data_sources).forEach(function (series_cols, index) {\n\n                    data_sources[series_cols].forEach(function (col, index) {\n                        data.push(totalValues[col])\n                        if (chartOptions.type !== \"line\") {\n                            series.push({\n                                'name': response.metadata.time_series_column_verbose_names[index],\n                                data: [totalValues[col]]\n                            })\n                        }\n                    })\n\n                })\n                if (chartOptions.type === \"line\") {\n                    series.push({\n                        'name': chartOptions.title,\n                        data: data\n                    })\n                }\n\n            }\n            return {\n                'categories': response.metadata.time_series_column_verbose_names,\n                'titles': response.metadata.time_series_column_verbose_names,\n                'series': series,\n            }\n        }\n\n        function get_crosstab_data(response, chartOptions) {\n\n            let series = []\n            let data_sources = {};\n            let col_dict = dataArrayToObject(response.columns, 'name')\n            chartOptions.data_source.forEach(function (elem, key) {\n                data_sources[elem] = [];\n                response.columns.forEach(function (col, key) {\n                    if (col.computation_field === elem) {\n                        data_sources[elem].push(col.name)\n                    }\n                })\n            })\n            if (!chartOptions.plot_total) {\n                response.data.forEach(function (elem, index) {\n                    Object.keys(data_sources).forEach(function (series_cols, index) {\n                        let data = []\n                        data_sources[series_cols].forEach(function (col, index) {\n                            data.push(elem[col])\n                        })\n                        series.push({\n                            'name': elem[chartOptions.title_source],\n                            'data': data\n                        })\n                    })\n                })\n            } else {\n                let all_column_to_be_summed = []\n                Object.keys(data_sources).forEach(function (series_cols, index) {\n                    all_column_to_be_summed = all_column_to_be_summed.concat(data_sources[series_cols]);\n                })\n                let totalValues = $.slick_reporting.calculateTotalOnObjectArray(response.data, all_column_to_be_summed)\n\n                Object.keys(data_sources).forEach(function (series_cols, index) {\n                    data_sources[series_cols].forEach(function (col, index) {\n                        series.push({\n                            'name': col_dict[col].verbose_name,\n                            'data': [totalValues[col]]\n                        })\n                    })\n                })\n            }\n            return {\n                'categories': response.metadata.crosstab_column_verbose_names,\n                'titles': response.metadata.crosstab_column_verbose_names,\n                'series': series,\n            }\n        }\n\n\n        function is_timeseries_support(response, chartOptions) {\n            if (chartOptions.time_series_support === false) return false;\n            return response.metadata.time_series_pattern || ''\n        }\n\n        function is_crosstab_support(response, chartOptions) {\n            return response.metadata.crosstab_model || ''\n        }\n\n        function displayChart(data, $elem, chartOptions) {\n            if ($elem.find(\"div[data-inner-chart-container]\").length === 0) {\n                $elem.append('<div data-inner-chart-container style=\"width:100%; height:400px;\"></div>')\n            }\n\n            let chartContainer = $elem.find(\"div[data-inner-chart-container]\")[0];\n\n            let cache_key = $.slick_reporting.get_xpath($elem) + \":\" + data.report_slug;\n            try {\n                let existing_chart = _chart_cache[cache_key];\n                if (typeof (existing_chart) !== 'undefined') {\n                    existing_chart.destroy()\n                }\n            } catch (e) {\n                console.error(e)\n            }\n\n            let chartObject = $.slick_reporting.highcharts.createChartObject(data, chartOptions);\n            _chart_cache[cache_key] = Highcharts.chart(chartContainer, chartObject);\n\n        }\n\n        $.slick_reporting.highcharts = {\n            createChartObject: createChartObject,\n            displayChart: displayChart,\n            defaults: {\n                normalStackedTooltipFormatter: normalStackedTooltipFormatter,\n                messages: {\n                    noData: 'No Data to display ... :-/',\n                    total: 'Total',\n                    percent: 'Percent',\n                },\n                credits: {\n                    // text: '',\n                    // href: ''\n                },\n                enable3d: false,\n            }\n        };\n    }\n\n    (jQuery)\n);\n"
  },
  {
    "path": "slick_reporting/static/slick_reporting/slick_reporting.js",
    "content": "(function ($) {\n\n    function executeFunctionByName(functionName, context /*, args */) {\n        let args = Array.prototype.slice.call(arguments, 2);\n        let namespaces = functionName.split(\".\");\n        let func = namespaces.pop();\n        for (let i = 0; i < namespaces.length; i++) {\n            context = context[namespaces[i]];\n        }\n        try {\n            func = context[func];\n            if (typeof func == 'undefined') {\n                throw `Function ${functionName} is not found in the context ${context}`\n            }\n\n        } catch (err) {\n            console.error(`Function ${functionName} is not found in the context ${context}`, err)\n        }\n        return func.apply(context, args);\n    }\n\n    function getObjFromArray(objList, obj_key, key_value, failToFirst) {\n        failToFirst = typeof (failToFirst) !== 'undefined';\n        if (key_value !== '') {\n            for (let i = 0; i < objList.length; i++) {\n                if (objList[i][obj_key] === key_value) {\n                    return objList[i];\n                }\n            }\n        }\n        if (failToFirst && objList.length > 0) {\n            return objList[0]\n        }\n\n        return false;\n    }\n\n    function calculateTotalOnObjectArray(data, columns) {\n        // Compute totals in array of objects\n        // example :\n        // calculateTotalOnObjectArray ([{ value1:500, value2: 70} , {value:200, value2:15} ], ['value'])\n        // return {'value1': 700, value2:85}\n\n        let total_container = {};\n        for (let r = 0; r < data.length; r++) {\n\n            for (let i = 0; i < columns.length; i++) {\n                if (typeof total_container[columns[i]] == 'undefined') {\n                    total_container[columns[i]] = 0;\n                }\n                let val = data[r][columns[i]];\n                if (val === '-') val = 0;\n\n                else if (typeof (val) == 'string') {\n                    try {\n                        val = val.replace(/,/g, '');\n                    } catch (err) {\n                        console.log(err, val, typeof (val));\n                    }\n                }\n                total_container[columns[i]] += parseFloat(val);\n            }\n        }\n        return total_container;\n    }\n\n    function get_xpath($element, forceTree) {\n        if ($element.length === 0) {\n            return null;\n        }\n\n        let element = $element[0];\n\n        if ($element.attr('id') && ((forceTree === undefined) || !forceTree)) {\n            return '//*[@id=\"' + $element.attr('id') + '\"]';\n        } else {\n            let paths = [];\n            for (; element && element.nodeType === Node.ELEMENT_NODE; element = element.parentNode) {\n                let index = 0;\n                for (let sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {\n                    if (sibling.nodeType === Node.DOCUMENT_TYPE_NODE)\n                        continue;\n                    if (sibling.nodeName === element.nodeName)\n                        ++index;\n                }\n\n                var tagName = element.nodeName.toLowerCase();\n                var pathIndex = (index ? '[' + (index + 1) + ']' : '');\n                paths.splice(0, 0, tagName + pathIndex);\n            }\n\n            return paths.length ? '/' + paths.join('/') : null;\n        }\n    }\n\n\n    $.slick_reporting = {\n        'getObjFromArray': getObjFromArray,\n        'calculateTotalOnObjectArray': calculateTotalOnObjectArray,\n        \"executeFunctionByName\": executeFunctionByName,\n        \"get_xpath\": get_xpath,\n        defaults: {\n            total_label: 'Total',\n        }\n\n    }\n    $.slick_reporting.cache = {}\n\n}(jQuery));"
  },
  {
    "path": "slick_reporting/static/slick_reporting/slick_reporting.report_loader.js",
    "content": "/*jshint esversion: 6 */\n\n/**\n * Created by ramezashraf on 13/08/16.\n */\n\n(function ($) {\n    let settings = {};\n\n    function failFunction(data, $elem) {\n        if (data.status === 403) {\n            $elem.hide()\n        } else {\n            console.log(data, $elem)\n        }\n    }\n\n    function loadComponents(data, $elem) {\n        let chartElem = $elem.find('[data-report-chart]');\n        let chart_id = $elem.attr('data-chart-id');\n        let display_chart_selector = $elem.attr('data-display-chart-selector');\n        if (chartElem.length !== 0 && data.chart_settings.length !== 0) {\n\n            $.slick_reporting.report_loader.displayChart(data, chartElem, chart_id);\n        }\n\n        if (display_chart_selector !== \"False\" && data.chart_settings.length > 1) {\n            $.slick_reporting.report_loader.createChartsUIfromResponse(data, $elem);\n        }\n\n        let tableElem = $elem.find('[data-report-table]');\n        if (tableElem.length !== 0) {\n            $.slick_reporting.datatable.buildAdnInitializeDatatable(data, tableElem);\n        }\n\n    }\n\n    function displayChart(data, $elem, chart_id) {\n        let engine = \"highcharts\";\n        let chartOptions = $.slick_reporting.getObjFromArray(data.chart_settings, 'id', chart_id, true);\n        let entryPoint = chartOptions.entryPoint || $.slick_reporting.report_loader.chart_engines[engine];\n        try {\n            $.slick_reporting.executeFunctionByName(entryPoint, window, data, $elem, chartOptions);\n        } catch (e) {\n            // Chart engine is not found or some error in the chart rendering,\n            // we catch it and display an error message instead of breaking the entire report\n            $(\"<p class='text-error'>Chart could not be loaded:  <code>\" + e + \"</code></p> \").insertAfter($elem);\n            $elem.remove();\n            console.error(e);\n        }\n    }\n\n\n    function refreshReportWidget($elem, extra_params) {\n        let successFunctionName = $elem.attr('data-success-callback');\n        successFunctionName = successFunctionName || \"$.slick_reporting.report_loader.successCallback\";\n        let failFunctionName = $elem.attr('data-fail-callback');\n        failFunctionName = failFunctionName || \"$.slick_reporting.report_loader.failFunction\";\n\n        let data = {};\n\n        let url = $elem.attr('data-report-url');\n        extra_params = extra_params || ''\n        let extraParams = extra_params + ($elem.attr('data-extra-params') || '');\n\n        let formSelector = $elem.attr('data-form-selector');\n        if (formSelector) {\n            data = $(formSelector).serialize();\n        } else {\n            if (url === '#') return; // there is no actual url, probably not enough permissions\n\n            if (extraParams !== '') {\n                url = url + \"?\" + extraParams;\n            }\n\n        }\n\n        $.get(url, data, function (data) {\n            $.slick_reporting.cache[data['report_slug']] = jQuery.extend(true, {}, data);\n            $.slick_reporting.executeFunctionByName(successFunctionName, window, data, $elem);\n        }).fail(function (data) {\n            $.slick_reporting.executeFunctionByName(failFunctionName, window, data, $elem);\n        });\n\n    }\n\n\n    function initialize() {\n        settings = JSON.parse(document.getElementById('slick_reporting_settings').textContent);\n        let chartSettings = {};\n        $('[data-report-widget]').not('[data-no-auto-load]').each(function (i, elem) {\n            refreshReportWidget($(elem));\n        });\n\n        Object.keys(settings[\"CHARTS\"]).forEach(function (key) {\n            chartSettings[key] = settings.CHARTS[key].entryPoint;\n        })\n        $.slick_reporting.report_loader.chart_engines = chartSettings;\n        $.slick_reporting.defaults.total_label = settings[\"MESSAGES\"][\"TOTAL_LABEL\"];\n    }\n\n    function _get_chart_icon(chart_type) {\n        try {\n            return \"<i class='\" + settings.FONT_AWESOME.ICONS[chart_type] + \"'></i>\";\n        } catch (e) {\n            console.error(e);\n        }\n        return '';\n    }\n\n    function createChartsUIfromResponse(data, $elem, a_class) {\n        a_class = typeof a_class == 'undefined' ? 'groupChartController' : a_class;\n        let $container = $('<div></div>');\n\n        let chartList = data['chart_settings'];\n        let report_slug = data['report_slug'];\n        $elem.find('.groupChartControllers').remove();\n        if (chartList.length !== 0) {\n            $container.append('<div class=\"groupChartControllers\">' +\n                '<ul class=\"nav nav-charts\"></ul></div>');\n        }\n        var ul = $container.find('ul');\n        for (var i = 0; i < chartList.length; i++) {\n            var icon;\n            var chart = chartList[i];\n            if (chart.disabled) continue;\n\n            let chart_type = chart.type;\n            icon = _get_chart_icon(chart_type);\n\n            ul.append('<li class=\"nav-link\"><a href class=\"' + a_class + '\" data-chart-id=\"' + chart.id + '\" ' +\n                'data-report-slug=\"' + report_slug + '\">' + icon + ' ' + chart.title + '</a></li>')\n        }\n        $elem.prepend($container)\n        return $container\n    }\n\n\n    jQuery(document).ready(function () {\n        $.slick_reporting.report_loader.initialize();\n        $('body').on('click', 'a[data-chart-id]', function (e) {\n            e.preventDefault();\n            let $this = $(this);\n            let data = $.slick_reporting.cache[$this.attr('data-report-slug')]\n            let chart_id = $this.attr('data-chart-id')\n            $.slick_reporting.report_loader.displayChart(data, $this.parents('[data-report-widget]').find('[data-report-chart]'), chart_id)\n\n        });\n\n        $('[data-export-btn]').on('click', function (e) {\n            let $elem = $(this);\n            e.preventDefault();\n            let form = $($elem.attr('data-form-selector'));\n            let url = '?' + form.serialize() + '&_export=' + $elem.attr('data-export-parameter');\n            if ($elem.attr('data-export-new-window')) {\n                window.open(url, '_blank');\n            } else {\n                window.location = url;\n            }\n        });\n        $('[data-get-results-button]').not(\".vanilla-btn-flag\").on('click', function (event) {\n            event.preventDefault();\n            let $elem = $('[data-report-widget]')\n            $.slick_reporting.report_loader.refreshReportWidget($elem)\n        });\n\n    });\n\n\n    $.slick_reporting.report_loader = {\n        cache: $.slick_reporting.cache,\n        // \"extractDataFromResponse\": extractDataFromResponse,\n        initialize: initialize,\n        refreshReportWidget: refreshReportWidget,\n        failFunction: failFunction,\n        displayChart: displayChart,\n        createChartsUIfromResponse: createChartsUIfromResponse,\n        successCallback: loadComponents,\n\n    }\n})(jQuery);"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/base.html",
    "content": "<!doctype html>\n{% load static %}\n{% load crispy_forms_tags %}\n\n<html lang=\"en\">\n<head>\n    <!-- Required meta tags -->\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\n    {% block extra_head %}\n        <!-- Bootstrap CSS -->\n        <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\"\n              integrity=\"sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC\"\n              crossorigin=\"anonymous\">\n        <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js\"\n                integrity=\"sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM\"\n                crossorigin=\"anonymous\"></script>\n    {% endblock %}\n\n    <title>{{ report_title }} | Django Slick Reporting</title>\n</head>\n<body>\n<div class=\"container\">\n\n\n    <div class=\"py-5 \">\n        <h2 class=\"page-title\"> {{ report_title }} </h2>\n        {% block content %}\n\n        {% endblock %}\n    </div>\n</div>\n\n\n\n{% block extrajs %}\n    {% include \"slick_reporting/js_resources.html\" %}\n\n{% endblock %}\n</body>\n</html>"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/js_resources.html",
    "content": "{% load i18n static slick_reporting_tags %}\n\n{% get_slick_reporting_settings as slick_reporting_settings %}\n\n{% add_jquery %}\n{% get_slick_reporting_media as media %}\n{{ media }}\n\n<link rel=\"stylesheet\" type=\"text/css\"\n      href=\"{{ slick_reporting_settings.FONT_AWESOME.CSS_URL }}\"/>\n\n{{ slick_reporting_settings|json_script:\"slick_reporting_settings\" }}\n"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/print_report.html",
    "content": "{% load i18n %}\n{% get_current_language as LANGUAGE_CODE %}\n{% get_current_language_bidi as LANGUAGE_BIDI %}\n<!DOCTYPE html>\n<html lang=\"{{ LANGUAGE_CODE }}\" {% if LANGUAGE_BIDI %}dir=\"rtl\"{% endif %}>\n<head>\n    <meta charset=\"utf-8\">\n    <title>{{ report_title }}</title>\n    <style>\n        body { font-family: sans-serif; font-size: 13px; margin: 1em 2em; }\n        h2 { text-align: center; margin-bottom: 0.25em; }\n        table { width: 100%; border-collapse: collapse; margin-top: 1em; }\n        th, td { border: 1px solid #ccc; padding: 4px 8px; text-align: start; }\n        th { background: #f0f0f0; font-weight: 600; }\n        .no-print { margin-bottom: 1em; }\n        @media print { .no-print { display: none; } }\n    </style>\n</head>\n<body>\n\n    {% include \"slick_reporting/print_report_controls.html\" %}\n\n    {% include \"slick_reporting/print_report_header.html\" %}\n\n    <h2>{{ report_title }}</h2>\n\n    <table>\n        <thead>\n            <tr>\n                {% for header in headers %}<th>{{ header }}</th>{% endfor %}\n            </tr>\n        </thead>\n        <tbody>\n            {% for row in rows %}\n            <tr>\n                {% for cell in row %}<td>{{ cell }}</td>{% endfor %}\n            </tr>\n            {% endfor %}\n        </tbody>\n    </table>\n\n    {% include \"slick_reporting/print_report_footer.html\" %}\n\n    <script>window.print();</script>\n</body>\n</html>\n"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/print_report_controls.html",
    "content": "{% load i18n %}\n{# Override this template to customise the on-screen controls shown above the printed page. #}\n<div class=\"no-print\" style=\"margin-bottom: 1em;\">\n    <button onclick=\"window.print()\">{% translate \"Print\" %}</button>\n</div>\n"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/print_report_footer.html",
    "content": "{# Override this template in your project to add a custom print footer (e.g. signatures, notes, page numbers). #}\n{# Context available: report_title, headers, rows #}\n"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/print_report_header.html",
    "content": "{# Override this template in your project to add a custom print header (e.g. company name, logo, date range). #}\n{# Context available: report_title, headers, rows #}\n"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/report.html",
    "content": "{% extends 'slick_reporting/base.html' %}\n{% load crispy_forms_tags i18n slick_reporting_tags %}\n\n{% block content %}\n    <div class=\"col-12\">\n        {% if form %}\n            {% include \"slick_reporting/report_form.html\" %}\n        {% endif %}\n\n        <div class=\"card\" id=\"{{ report.report_slug }}\">\n            <div class=\"card-header\">\n                <h5 class=\"card-title\">{% translate \"Results\" %}</h5>\n            </div>\n            <div class=\"card-body\">\n                <div data-report-widget\n                     data-report-url=\"{{ request.path }}\"\n                     data-extra-params=\"\"\n                     data-form-selector=\"#reportForm\"\n                        {% if not auto_load %} data-no-auto-load{% endif %}\n                     data-display-chart-selector=\"True\">\n                    <div data-report-chart>\n                    </div>\n                    <div data-report-table>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n\n{% endblock %}\n\n{% block extrajs %}\n    {{ block.super }}\n    {% get_charts_media report.get_chart_settings %}\n{% endblock %}"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/report_form.html",
    "content": "{% load i18n crispy_forms_tags %}\n<form id=\"reportForm\" class=\"card\">\n    <div class=\"card-header\">\n        <h3 class=\"card-title\">{% translate \"Filters\" %}</h3>\n    </div>\n    <div class=\"card-body\">\n        {% if form and crispy_helper %}\n            {% crispy form crispy_helper %}\n        {% else %}\n            {% crispy form %}\n        {% endif %}\n    </div>\n    <div class=\"card-footer text-end\">\n        <input type=\"submit\" value=\"{% translate \"Filter\" %}\"\n               class=\"btn btn-primary  refreshReport\" data-get-results-button>\n\n\n        {% for export_action in report.get_export_actions %}\n            <button class=\"btn {{ export_action.css_class }}\" data-export-btn\n                    data-export-parameter=\"{{ export_action.parameter }}\" data-form-selector=\"#reportForm\"\n                    {% if export_action.new_window %}data-export-new-window=\"true\"{% endif %}>\n                {% if export_action.icon %}<i class=\"{{ export_action.icon }}\"></i> {% endif %}\n                {{ export_action.title }} </button>\n        {% endfor %}\n    </div>\n</form>"
  },
  {
    "path": "slick_reporting/templates/slick_reporting/widget_template.html",
    "content": "{% load  slick_reporting_tags %}\n\n\n<div class=\"card\">\n    {% if display_title %}\n        <div class=\"card-header\">\n            <h5 class=\"card-title\">{{ title }}</h5>\n        </div>\n    {% endif %}\n    <div class=\"card-body\">\n        <div data-report-widget\n             data-report-url=\"{{ report_url }}\"\n             data-extra-params=\"{{ extra_params|default:'' }}\"\n             data-success-callback=\"{{ success_callback|default:'' }}\"\n             data-chart-id=\"{{ chart_id|default:'' }}\"\n             data-display-chart-selector=\"{{ display_chart_selector }}\"\n             data-failure-callback=\"{{ failure_callback|default:'' }}\"\n        >\n            {% block widget_content %}\n                {% if display_chart %}\n                    <div data-report-chart></div>\n                {% endif %}\n                {% if display_table %}\n                    <div data-report-table>\n                    </div>\n                {% endif %}\n            {% endblock %}\n\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "slick_reporting/templatetags/__init__.py",
    "content": "\n"
  },
  {
    "path": "slick_reporting/templatetags/slick_reporting_tags.py",
    "content": "from django import template\nfrom django.template.loader import get_template\nfrom django.forms import Media\nfrom django.templatetags.static import static\nfrom django.urls import reverse, resolve\nfrom django.utils.safestring import mark_safe\n\nfrom ..app_settings import SLICK_REPORTING_JQUERY_URL, SLICK_REPORTING_SETTINGS, get_media\n\nregister = template.Library()\n\n\ndef _resolve_static(path):\n    \"\"\"Return an absolute URL for a static asset path, mirroring Media.absolute_path().\"\"\"\n    if path.startswith((\"http://\", \"https://\", \"/\")):\n        return path\n    return static(path)\n\n\n@register.simple_tag\ndef get_widget_from_url(url_name=None, url=None, **kwargs):\n    _url = \"\"\n    if not (url_name or url):\n        raise ValueError(\"url_name or url must be provided\")\n    if url_name:\n        url = reverse(url_name)\n    view = resolve(url)\n    kwargs[\"report\"] = view.func.view_class\n    kwargs[\"report_url\"] = url\n    return get_widget(**kwargs)\n\n\n@register.simple_tag\ndef get_widget(report, template_name=\"\", url_name=\"\", report_url=None, **kwargs):\n    kwargs[\"report\"] = report\n    if not report:\n        raise ValueError(\"report argument is empty. Are you sure you're using the correct report name\")\n    if not (report_url or url_name):\n        raise ValueError(\"report_url or url_name must be provided\")\n\n    # if not report.chart_settings:\n    kwargs.setdefault(\"display_chart\", bool(report.chart_settings))\n    kwargs.setdefault(\"display_table\", True)\n\n    kwargs.setdefault(\"display_chart_selector\", kwargs[\"display_chart\"])\n    kwargs.setdefault(\"display_title\", True)\n\n    passed_title = kwargs.get(\"title\", None)\n    kwargs[\"title\"] = passed_title or report.get_report_title()\n    kwargs[\"report_url\"] = report_url\n    if not report_url:\n        kwargs[\"report_url\"] = reverse(url_name)\n\n    kwargs.setdefault(\"extra_params\", \"\")\n\n    template = get_template(template_name or \"slick_reporting/widget_template.html\")\n\n    return template.render(context=kwargs)\n\n\n@register.simple_tag\ndef add_jquery():\n    if SLICK_REPORTING_JQUERY_URL:\n        url = _resolve_static(SLICK_REPORTING_JQUERY_URL)\n        return mark_safe(f'<script src=\"{url}\"></script>')\n    return \"\"\n\n\n@register.simple_tag\ndef get_charts_media(chart_settings):\n    charts_dict = SLICK_REPORTING_SETTINGS[\"CHARTS\"]\n    media = Media()\n    if chart_settings == \"all\":\n        available_types = charts_dict.keys()\n    else:\n        available_types = [chart[\"engine_name\"] for chart in chart_settings]\n        available_types = set(available_types)\n\n    for type in available_types:\n        media += Media(css=charts_dict.get(type, {}).get(\"css\", {}), js=charts_dict.get(type, {}).get(\"js\", []))\n    return media\n\n\n@register.simple_tag\ndef get_slick_reporting_media():\n    from django.forms import Media\n\n    media = get_media()\n    return Media(css=media[\"css\"], js=media[\"js\"])\n\n\n@register.simple_tag\ndef get_slick_reporting_settings():\n    settings = dict(SLICK_REPORTING_SETTINGS)\n\n    media = dict(settings.get(\"MEDIA\", {}))\n    media[\"js\"] = [_resolve_static(p) for p in media.get(\"js\", [])]\n    settings[\"MEDIA\"] = media\n\n    charts = {}\n    for engine, config in settings.get(\"CHARTS\", {}).items():\n        config = dict(config)\n        config[\"js\"] = [_resolve_static(p) for p in config.get(\"js\", [])]\n        charts[engine] = config\n    settings[\"CHARTS\"] = charts\n\n    font_awesome = dict(settings.get(\"FONT_AWESOME\", {}))\n    if \"CSS_URL\" in font_awesome:\n        font_awesome[\"CSS_URL\"] = _resolve_static(font_awesome[\"CSS_URL\"])\n    settings[\"FONT_AWESOME\"] = font_awesome\n\n    return settings\n"
  },
  {
    "path": "slick_reporting/views.py",
    "content": "import csv\nimport datetime\nimport warnings\n\nimport simplejson as json\nfrom django import forms\nfrom django.conf import settings\nfrom django.contrib.auth.mixins import UserPassesTestMixin\nfrom django.db.models import Q\nfrom django.forms import modelform_factory\nfrom django.http import HttpResponse, StreamingHttpResponse, JsonResponse\nfrom django.utils.encoding import force_str\nfrom django.utils.functional import Promise\nfrom django.views.generic import FormView\n\nfrom .app_settings import SLICK_REPORTING_SETTINGS, get_access_function\nfrom .forms import (\n    report_form_factory,\n    get_crispy_helper,\n    default_formfield_callback,\n    OrderByForm,\n)\nfrom .generator import (\n    ReportGenerator,\n    ListViewReportGenerator,\n    ReportGeneratorAPI,\n    Chart,  # noqa # needed for easier importing in other apps\n)\n\n\ndef dictsort(value, arg, desc=False):\n    \"\"\"\n    Takes a list of dicts, returns that list sorted by the property given in\n    the argument.\n    \"\"\"\n    return sorted(value, key=lambda x: x[arg], reverse=desc)\n\n\nclass ExportToCSV(object):\n    def get_filename(self):\n        return self.report_title\n\n    def get_response(self):\n        response = HttpResponse(content_type=\"text/csv\")\n        response[\"Content-Disposition\"] = \"attachment; filename={filename}.csv\".format(filename=self.get_filename())\n\n        writer = csv.writer(response)\n        for rows in self.get_rows():\n            writer.writerow(rows)\n\n        return response\n\n    def get_rows(self):\n        columns, verbose_names = self.get_columns()\n        yield verbose_names\n        for line in self.report_data[\"data\"]:\n            yield [line[col_name] for col_name in columns]\n\n    def get_columns(self, extra_context=None):\n        return list(zip(*[(x[\"name\"], x[\"verbose_name\"]) for x in self.report_data[\"columns\"]]))\n\n    def __init__(self, request, report_data, report_title, **kwargs):\n        self.request = request\n        self.report_data = report_data\n        self.report_title = report_title\n        self.kwargs = kwargs\n\n\nclass ExportToStreamingCSV(ExportToCSV):\n    def get_response(self):\n        # Copied form Djagno Docs\n        class Echo:\n            def write(self, value):\n                return value\n\n        pseudo_buffer = Echo()\n        writer = csv.writer(pseudo_buffer)\n        return StreamingHttpResponse(\n            (writer.writerow(row) for row in self.get_rows()),\n            content_type=\"text/csv\",\n            headers={\n                \"Content-Disposition\": 'attachment; filename=\"{filename}.csv\"'.format(filename=self.get_filename())\n            },\n        )\n\n\nclass PrintHTMLExport:\n    template_name = \"slick_reporting/print_report.html\"\n\n    def __init__(self, request, report_data, report_title, **kwargs):\n        self.request = request\n        self.report_data = report_data\n        self.report_title = report_title\n\n    def get_response(self):\n        from django.shortcuts import render\n\n        columns = self.report_data.get(\"columns\", [])\n        headers = [col[\"verbose_name\"] for col in columns]\n        rows = [[row.get(col[\"name\"], \"\") for col in columns] for row in self.report_data.get(\"data\", [])]\n        return render(self.request, self.template_name, {\"report_title\": self.report_title, \"headers\": headers, \"rows\": rows})\n\n\nclass ReportViewBase(ReportGeneratorAPI, UserPassesTestMixin, FormView):\n    report_slug = None\n\n    report_title = \"\"\n\n    report_description = \"\"\n\n    report_title_context_key = \"report_title\"\n\n    report_generator_class = ReportGenerator\n\n    base_model = None\n\n    chart_settings = None\n\n    excluded_fields = None\n\n    time_series_selector = False\n    time_series_selector_choices = None\n    time_series_selector_default = None\n    time_series_selector_allow_empty = False\n\n    csv_export_class = ExportToStreamingCSV\n    print_export_class = PrintHTMLExport\n\n    with_type = False\n    doc_type_field_name = \"doc_type\"\n    doc_type_plus_list = None\n    doc_type_minus_list = None\n    auto_load = True\n    chart_engine = \"\"\n\n    default_order_by = \"\"\n\n    template_name = \"slick_reporting/report.html\"\n\n    export_actions = None\n\n    def test_func(self):\n        access_function = get_access_function()\n        return access_function(self)\n\n    @classmethod\n    def get_report_title(cls):\n        \"\"\"\n        :return: The report name\n        \"\"\"\n        name = cls.__name__\n        if cls.report_title:\n            name = cls.report_title\n        return name\n\n    def order_results(self, data):\n        \"\"\"\n        order the results based on GET parameter or default_order_by\n        :param data: List of Dict to be ordered\n        :return: Ordered data\n        \"\"\"\n        order_field, asc = OrderByForm(self.request.GET).get_order_by(self.default_order_by)\n        if order_field:\n            data = dictsort(data, order_field, asc)\n        return data\n\n    def get_doc_types_q_filters(self):\n        if self.doc_type_plus_list or self.doc_type_minus_list:\n            return (\n                [Q(**{f\"{self.doc_type_field_name}__in\": self.doc_type_plus_list})] if self.doc_type_plus_list else []\n            ), (\n                [Q(**{f\"{self.doc_type_field_name}__in\": self.doc_type_minus_list})] if self.doc_type_minus_list else []\n            )\n\n        return [], []\n\n    def get_export_actions(self):\n        \"\"\"\n        Hook to get the export options\n        :return: list of export options\n        \"\"\"\n        actions = []\n        if self.csv_export_class:\n            actions.append(\"export_csv\")\n        if self.print_export_class:\n            actions.append(\"export_print\")\n\n        if self.export_actions:\n            actions = actions + self.export_actions\n\n        export_actions = []\n\n        for action in actions:\n            func = getattr(self, action, None)\n            parameter = action.replace(\"export_\", \"\")\n\n            export_actions.append(\n                {\n                    \"name\": action,\n                    \"title\": getattr(func, \"title\", action.replace(\"_\", \" \").title()),\n                    \"icon\": getattr(func, \"icon\", \"\"),\n                    \"css_class\": getattr(func, \"css_class\", \"\"),\n                    \"parameter\": parameter,\n                    \"new_window\": getattr(func, \"new_window\", False),\n                }\n            )\n        return export_actions\n\n    def get(self, request, *args, **kwargs):\n        form_class = self.get_form_class()\n        self.form = self.get_form(form_class)\n        report_data = {}\n        if self.form.is_valid():\n            if self.request.GET or self.request.POST or request.headers.get(\"x-requested-with\") == \"XMLHttpRequest\":\n                # only display results if it's requested,\n                # considered requested if it's ajax request, or a populated GET or POST.\n                report_data = self.get_report_results()\n\n                export_option = request.GET.get(\"_export\", \"\")\n                if export_option:\n                    try:\n                        return getattr(self, f\"export_{export_option}\")(report_data)\n                    except AttributeError:\n                        pass\n\n                if request.headers.get(\"x-requested-with\") == \"XMLHttpRequest\":\n                    return self.ajax_render_to_response(report_data)\n\n            return self.render_to_response(self.get_context_data(report_data=report_data))\n        else:\n            return self.form_invalid(self.form)\n\n        # return self.render_to_response(self.get_context_data())\n\n    def export_csv(self, report_data):\n        return self.csv_export_class(self.request, report_data, self.report_title).get_response()\n\n    export_csv.title = SLICK_REPORTING_SETTINGS[\"MESSAGES\"][\"export_to_csv\"]\n    export_csv.css_class = \"btn btn-primary\"\n    export_csv.icon = \"\"\n\n    def export_print(self, report_data):\n        return self.print_export_class(self.request, report_data, self.get_report_title()).get_response()\n\n    export_print.title = SLICK_REPORTING_SETTINGS[\"MESSAGES\"][\"print_report\"]\n    export_print.css_class = \"btn btn-secondary\"\n    export_print.icon = \"\"\n    export_print.new_window = True\n\n    @classmethod\n    def get_report_model(cls):\n        if cls.queryset is not None:\n            return cls.queryset.model\n        if not cls.report_model and cls.table_name:\n            from .dynamic_model import get_dynamic_model\n\n            return get_dynamic_model(cls.table_name)\n        return cls.report_model\n\n    def ajax_render_to_response(self, report_data):\n        return HttpResponse(self.serialize_to_json(report_data), content_type=\"application/json\")\n\n    def serialize_to_json(self, response_data):\n        \"\"\"Returns the JSON string for the compiled data object.\"\"\"\n\n        def date_handler(obj):\n            if type(obj) is datetime.datetime:\n                return obj.strftime(\"%Y-%m-%d %H:%M\")\n            elif hasattr(obj, \"isoformat\"):\n                return obj.isoformat()\n            elif isinstance(obj, Promise):\n                return force_str(obj)\n\n        indent = None\n        if settings.DEBUG:\n            indent = 4\n\n        return json.dumps(response_data, indent=indent, use_decimal=True, default=date_handler)\n\n    def get_form_class(self):\n        \"\"\"\n        Automatically instantiate a form based on details provided\n        :return:\n        \"\"\"\n        return self.form_class or report_form_factory(\n            self.get_report_model(),\n            crosstab_model=self.crosstab_field,\n            display_compute_remainder=self.crosstab_compute_remainder,\n            excluded_fields=self.excluded_fields,\n            fkeys_filter_func=self.fkeys_filter_func_hook,\n            initial=self.get_initial(),\n            show_time_series_selector=self.time_series_selector,\n            time_series_selector_choices=self.time_series_selector_choices,\n            time_series_selector_default=self.time_series_selector_default,\n            time_series_selector_allow_empty=self.time_series_selector_allow_empty,\n            add_start_date=self.start_date_field_name or self.date_field,\n            add_end_date=self.end_date_field_name or self.date_field,\n        )\n\n    @staticmethod\n    def fkeys_filter_func_hook(fkeys_dict):\n        \"\"\"\n        A hook to customize which fileds to eliminate on the form\n        Example Useage:\n        ```\n            exclude_list = [\"owner_id\", \"polymorphic_ctype_id\", \"lastmod_user_id\"]\n            return {K:v for k,v in fkeys_dict.items() if k not in exclude_list}\n        ```\n        \"\"\"\n        return fkeys_dict\n\n    def get_form_kwargs(self):\n        \"\"\"\n        Returns the keyword arguments for instantiating the form.\n        \"\"\"\n        kwargs = {\n            \"initial\": self.get_initial(),\n            \"prefix\": self.get_prefix(),\n        }\n\n        if self.request.method in (\"POST\", \"PUT\"):\n            kwargs.update(\n                {\n                    \"data\": self.request.POST,\n                    \"files\": self.request.FILES,\n                }\n            )\n        elif self.request.method in (\"GET\", \"PUT\"):\n            if self.request.GET or self.request.headers.get(\"x-requested-with\") == \"XMLHttpRequest\":\n                kwargs.update(\n                    {\n                        \"data\": self.request.GET,\n                    }\n                )\n        return kwargs\n\n    def get_crosstab_ids(self):\n        \"\"\"\n        Hook to get the crosstab ids\n        :return:\n        \"\"\"\n        return self.form.get_crosstab_ids()\n\n    def get_group_by_custom_querysets(self):\n        return self.group_by_custom_querysets\n\n    def get_report_generator(self, queryset=None, for_print=False):\n        queryset = queryset or self.get_queryset()\n        q_filters, kw_filters = self.form.get_filters()\n        crosstab_compute_remainder = False\n        if self.crosstab_field:\n            self.crosstab_ids = self.get_crosstab_ids()\n        try:\n            crosstab_compute_remainder = (\n                self.form.get_crosstab_compute_remainder()\n                if self.crosstab_compute_remainder is None and (self.request.GET or self.request.POST)\n                else self.crosstab_compute_remainder\n            )\n        except NotImplementedError:\n            pass\n\n        time_series_pattern = self.time_series_pattern\n        if self.time_series_selector:\n            time_series_pattern = self.form.get_time_series_pattern()\n\n        doc_type_plus_list, doc_type_minus_list = [], []\n\n        if self.with_type:\n            doc_type_plus_list, doc_type_minus_list = self.get_doc_types_q_filters()\n\n        return self.report_generator_class(\n            self.get_report_model(),\n            start_date=self.form.get_start_date(),\n            end_date=self.form.get_end_date(),\n            q_filters=q_filters,\n            kwargs_filters=kw_filters,\n            date_field=self.date_field,\n            main_queryset=queryset,\n            print_flag=for_print,\n            limit_records=self.limit_records,\n            swap_sign=self.swap_sign,\n            columns=self.columns,\n            group_by=self.group_by,\n            group_by_custom_querysets=self.get_group_by_custom_querysets(),\n            group_by_custom_querysets_column_verbose_name=self.group_by_custom_querysets_column_verbose_name,\n            time_series_pattern=time_series_pattern,\n            time_series_columns=self.time_series_columns,\n            time_series_custom_dates=self.time_series_custom_dates,\n            crosstab_field=self.crosstab_field,\n            crosstab_ids=self.crosstab_ids,\n            crosstab_columns=self.crosstab_columns,\n            crosstab_compute_remainder=crosstab_compute_remainder,\n            crosstab_ids_custom_filters=self.crosstab_ids_custom_filters,\n            crosstab_precomputed=self.crosstab_precomputed,\n            format_row_func=self.format_row,\n            container_class=self,\n            doc_type_plus_list=doc_type_plus_list,\n            doc_type_minus_list=doc_type_minus_list,\n        )\n\n    def format_row(self, row_obj):\n        \"\"\"\n        A hook to format each row . This method gets called on each row in the results. <ust return the object\n        :param row_obj: a dict representing a single row in the results\n        :return: A dict representing a single row in the results\n        \"\"\"\n        return row_obj\n\n    @classmethod\n    def get_columns_data(cls, generator):\n        \"\"\"\n        Hook to get the columns information to front end\n        :param generator: the SlickReportGenerator instance used\n        :return:\n        \"\"\"\n        return generator.get_columns_data()\n\n    def get_report_results(self, for_print=False):\n        \"\"\"\n        Gets the reports Data, and, its meta data used by datatables.net and highcharts\n        :return: JsonResponse\n        \"\"\"\n\n        queryset = self.get_queryset()\n        report_generator = self.get_report_generator(queryset, for_print)\n        data = report_generator.get_report_data()\n        data = self.filter_results(data, for_print)\n        data = self.order_results(data)\n\n        return report_generator.get_full_response(\n            data=data,\n            report_slug=self.get_report_slug(),\n            chart_settings=self.chart_settings,\n            default_chart_title=self.report_title,\n            default_chart_engine=self.chart_engine,\n        )\n\n    @classmethod\n    def get_metadata(cls, generator):\n        \"\"\"\n        A hook to send data about the report for front end which can later be used in charting\n        :return:\n        \"\"\"\n        return generator.get_metadata()\n\n    def get_chart_settings(self, generator=None):\n        \"\"\"\n        Ensure the sane settings are passed to the front end.\n        \"\"\"\n        return self.report_generator_class.get_chart_settings(\n            chart_settings=self.chart_settings or [],\n            default_chart_title=self.report_title,\n            chart_engine=self.chart_engine,\n        )\n\n    @classmethod\n    def get_queryset(cls):\n        if cls.queryset is None:\n            return cls.get_report_model()._default_manager.all()\n        return cls.queryset\n\n    def filter_results(self, data, for_print=False):\n        \"\"\"\n        Hook to Filter results based on computed data (like eliminate __balance__ = 0, etc)\n        return None to remove the row from the results\n        :param data: List of objects\n        :param for_print: is print request\n        :return: filtered data\n        \"\"\"\n        return data\n\n    @classmethod\n    def get_report_slug(cls):\n        return cls.report_slug or cls.__name__.lower()\n\n    def get_initial(self):\n        initial = self.initial.copy()\n        initial.update(\n            {\n                \"start_date\": SLICK_REPORTING_SETTINGS[\"DEFAULT_START_DATE_TIME\"],\n                \"end_date\": SLICK_REPORTING_SETTINGS[\"DEFAULT_END_DATE_TIME\"],\n            }\n        )\n        return initial\n\n    def get_form_crispy_helper(self):\n        \"\"\"\n        A hook retuning crispy helper for the form\n        :return:\n        \"\"\"\n        if hasattr(self, \"form\"):\n            return self.form.get_crispy_helper()\n        return None\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context[self.report_title_context_key] = self.get_report_title()\n        context[\"crispy_helper\"] = self.get_form_crispy_helper()\n        context[\"auto_load\"] = self.auto_load\n        context[\"report\"] = self\n\n        if not (self.request.POST or self.request.GET):\n            context[\"form\"] = self.get_form_class()(**self.get_form_kwargs())\n        return context\n\n    def form_invalid(self, form):\n        if self.request.META.get(\"HTTP_X_REQUESTED_WITH\") == \"XMLHttpRequest\":\n            return JsonResponse(form.errors, status=400)\n        return super().form_invalid(form)\n\n\nclass ReportView(ReportViewBase):\n    def __init_subclass__(cls) -> None:\n        # Skip early validation for table_name-based (dynamic) views: the model\n        # requires live DB introspection which must not happen at import time\n        # (e.g. during `manage.py migrate`). Validation runs at request time via\n        # ReportGenerator._parse().\n        if cls.columns and not cls.table_name:\n            cls.report_generator_class.check_columns(\n                cls,\n                cls.columns,\n                cls.group_by,\n                cls.get_report_model(),\n                container_class=cls,\n                group_by_custom_querysets=cls.group_by_custom_querysets,\n            )\n\n        super().__init_subclass__()\n\n\nclass SlickReportingListViewMixin(ReportViewBase):\n    report_generator_class = ListViewReportGenerator\n    filters = None\n\n    def get_queryset(self):\n        qs = self.queryset or self.report_model.objects\n        if self.default_order_by:\n            qs.order_by(self.default_order_by)\n        return qs\n\n    def get_form_filters(self, form):\n        if hasattr(form, \"get_filters\"):\n            return form.get_filters()\n\n        kw_filters = {}\n\n        for name, field in form.base_fields.items():\n            if type(field) is forms.ModelMultipleChoiceField:\n                value = form.cleaned_data[name]\n                if value:\n                    kw_filters[f\"{name}__in\"] = form.cleaned_data[name]\n            elif type(field) is forms.BooleanField:\n                # boolean field while checked on frontend , and have initial = True, give false value on cleaned_data\n                #  Hence this check to see if it was indeed in the GET params,\n                value = field.initial\n                if self.request.GET:\n                    value = form.cleaned_data.get(name, False)\n                kw_filters[name] = value\n\n            else:\n                value = form.cleaned_data[name]\n                if value:\n                    kw_filters[name] = form.cleaned_data[name]\n\n        return [], kw_filters\n\n    def get_form_crispy_helper(self):\n        return get_crispy_helper(self.filters)\n\n    def get_report_generator(self, queryset=None, for_print=False):\n        q_filters, kw_filters = self.get_form_filters(self.form)\n\n        return self.report_generator_class(\n            self.get_report_model(),\n            # start_date=self.form.get_start_date(),\n            # end_date=self.form.get_end_date(),\n            q_filters=q_filters,\n            kwargs_filters=kw_filters,\n            date_field=self.date_field,\n            main_queryset=queryset,\n            print_flag=for_print,\n            limit_records=self.limit_records,\n            columns=self.columns,\n            format_row_func=self.format_row,\n            container_class=self,\n        )\n\n    def get_form_class(self):\n        if self.form_class:\n            return self.form_class\n\n        elif self.filters:\n            return modelform_factory(\n                model=self.get_report_model(),\n                fields=self.filters,\n                formfield_callback=default_formfield_callback,\n            )\n\n        return report_form_factory(\n                self.get_report_model(),\n                crosstab_model=self.crosstab_field,\n                display_compute_remainder=self.crosstab_compute_remainder,\n                excluded_fields=self.excluded_fields,\n                fkeys_filter_func=self.fkeys_filter_func_hook,\n                initial=self.get_initial(),\n                show_time_series_selector=self.time_series_selector,\n                time_series_selector_choices=self.time_series_selector_choices,\n                time_series_selector_default=self.time_series_selector_default,\n                time_series_selector_allow_empty=self.time_series_selector_allow_empty,\n                add_start_date=self.start_date_field_name or self.date_field,\n                add_end_date=self.end_date_field_name or self.date_field,\n            )\n\n    def get_report_results(self, for_print=False):\n        \"\"\"\n        Gets the reports Data, and, its meta data used by datatables.net and highcharts\n        :return: JsonResponse\n        \"\"\"\n\n        queryset = self.get_queryset()\n        report_generator = self.get_report_generator(queryset, for_print)\n        data = report_generator.get_report_data()\n        data = self.filter_results(data, for_print)\n\n        return report_generator.get_full_response(\n            data=data,\n            report_slug=self.get_report_slug(),\n            chart_settings=self.chart_settings,\n            default_chart_title=self.report_title,\n        )\n\n\nclass SlickReportingListView(SlickReportingListViewMixin, ReportViewBase):\n    def __init_subclass__(cls) -> None:\n        warnings.warn(\n            \"slick_reporting.view.SlickReportingListView is\"\n            \"deprecated in favor of slick_reporting.view.ListReportView\",\n            Warning,\n            stacklevel=2,\n        )\n        super().__init_subclass__()\n\n\nclass ListReportView(SlickReportingListViewMixin):\n    pass\n\n\nclass SlickReportViewBase(ReportViewBase):\n    \"\"\"\n    Deprecated in favor of slick_reporting.view.ReportViewBase\n    \"\"\"\n\n    def __init_subclass__(cls) -> None:\n        warnings.warn(\n            \"slick_reporting.view.SlickReportView and slick_reporting.view.SlickReportViewBase are \"\n            \"deprecated in favor of slick_reporting.view.ReportView and slick_reporting.view.BaseReportView\",\n            Warning,\n            stacklevel=2,\n        )\n\n        super().__init_subclass__()\n\n\nclass SlickReportView(ReportView):\n    def __init_subclass__(cls) -> None:\n        warnings.warn(\n            \"slick_reporting.view.SlickReportView and slick_reporting.view.SlickReportViewBase are \"\n            \"deprecated in favor of slick_reporting.view.ReportView and slick_reporting.view.BaseReportView\",\n            Warning,\n            stacklevel=2,\n        )\n\n        # cls.report_generator_class.check_columns(\n        #     cls.columns, cls.group_by, cls.get_report_model(), container_class=cls\n        # )\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "\n"
  },
  {
    "path": "tests/models.py",
    "content": "from django.contrib.contenttypes.fields import GenericForeignKey\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.db import models\n\nfrom django.utils.translation import gettext_lazy as _\n\n\nclass Product(models.Model):\n    CATEGORY_CHOICES = (\n        (\"tiny\", \"tiny\"),\n        (\"small\", \"small\"),\n        (\"medium\", \"medium\"),\n        (\"big\", \"big\"),\n    )\n    slug = models.CharField(max_length=200, verbose_name=_(\"Slug\"))\n    name = models.CharField(max_length=200, verbose_name=_(\"Name\"))\n    sku = models.CharField(max_length=200, default=\"\", blank=True)\n    category = models.CharField(max_length=10, choices=CATEGORY_CHOICES)\n    notes = models.TextField()\n\n    class Meta:\n        verbose_name = _(\"Product\")\n        verbose_name_plural = _(\"Products\")\n\n\nclass ProductCustomID(models.Model):\n    CATEGORY_CHOICES = (\n        (\"tiny\", \"tiny\"),\n        (\"small\", \"small\"),\n        (\"medium\", \"medium\"),\n        (\"big\", \"big\"),\n    )\n    hash = models.AutoField(primary_key=True)\n    slug = models.CharField(max_length=200, verbose_name=_(\"Slug\"))\n    name = models.CharField(max_length=200, verbose_name=_(\"Name\"))\n    sku = models.CharField(max_length=200, default=\"\", blank=True)\n    category = models.CharField(max_length=10, choices=CATEGORY_CHOICES)\n    notes = models.TextField()\n\n    class Meta:\n        verbose_name = _(\"Product\")\n        verbose_name_plural = _(\"Products\")\n\n\nclass Agent(models.Model):\n    name = models.CharField(max_length=200, verbose_name=_(\"Name\"))\n\n\nclass Contact(models.Model):\n    address = models.CharField(max_length=200, verbose_name=_(\"Name\"))\n    po_box = models.CharField(\n        max_length=200, verbose_name=_(\"po_box\"), null=True, blank=True\n    )\n    agent = models.ForeignKey(Agent, on_delete=models.CASCADE)\n\n\nclass Client(models.Model):\n    class SexChoices(models.TextChoices):\n        FEMALE = \"FEMALE\", _(\"Female\")\n        MALE = \"MALE\", _(\"Male\")\n        OTHER = \"OTHER\", _(\"Other\")\n\n    slug = models.CharField(max_length=200, verbose_name=_(\"Client Slug\"))\n\n    name = models.CharField(max_length=200, verbose_name=_(\"Name\"), unique=True)\n    email = models.EmailField(blank=True)\n    notes = models.TextField()\n    contact = models.ForeignKey(Contact, on_delete=models.CASCADE, null=True)\n    sex = models.CharField(max_length=10, choices=SexChoices.choices, default=\"OTHER\")\n\n    class Meta:\n        verbose_name = _(\"Client\")\n        verbose_name_plural = _(\"Clients\")\n\n\nclass SimpleSales(models.Model):\n    slug = models.SlugField()\n    doc_date = models.DateTimeField(_(\"date\"), db_index=True)\n    client = models.ForeignKey(Client, on_delete=models.CASCADE)\n    product = models.ForeignKey(Product, on_delete=models.CASCADE)\n    quantity = models.DecimalField(\n        _(\"quantity\"), max_digits=19, decimal_places=2, default=0\n    )\n    price = models.DecimalField(_(\"price\"), max_digits=19, decimal_places=2, default=0)\n    value = models.DecimalField(_(\"value\"), max_digits=19, decimal_places=2, default=0)\n    created_at = models.DateTimeField(null=True, verbose_name=_(\"Created at\"))\n    flag = models.CharField(max_length=50, default=\"sales\")\n\n    content_type = models.ForeignKey(\n        ContentType, on_delete=models.DO_NOTHING, null=True\n    )\n    object_id = models.PositiveIntegerField(null=True)\n    content_object = GenericForeignKey(\"content_type\", \"object_id\")\n\n    def save(\n            self, *args, **kwargs\n    ):\n        self.value = self.quantity * self.price\n        super().save(*args, **kwargs)\n\n    class Meta:\n        verbose_name = _(\"Sale\")\n        verbose_name_plural = _(\"Sales\")\n        ordering = [\"-created_at\"]\n\n\nclass SimpleSales2(models.Model):\n    slug = models.SlugField()\n    doc_date = models.DateTimeField(_(\"date\"), db_index=True)\n    client = models.ForeignKey(Client, on_delete=models.CASCADE, to_field=\"name\")\n    product = models.ForeignKey(Product, on_delete=models.CASCADE)\n    quantity = models.DecimalField(\n        _(\"quantity\"), max_digits=19, decimal_places=2, default=0\n    )\n    price = models.DecimalField(_(\"price\"), max_digits=19, decimal_places=2, default=0)\n    value = models.DecimalField(_(\"value\"), max_digits=19, decimal_places=2, default=0)\n    created_at = models.DateTimeField(null=True, verbose_name=_(\"Created at\"))\n    flag = models.CharField(max_length=50, default=\"sales\")\n\n    content_type = models.ForeignKey(\n        ContentType, on_delete=models.DO_NOTHING, null=True\n    )\n    object_id = models.PositiveIntegerField(null=True)\n    content_object = GenericForeignKey(\"content_type\", \"object_id\")\n\n    def save(\n            self, *args, **kwargs\n    ):\n        self.value = self.quantity * self.price\n        super().save(*args, **kwargs)\n\n    class Meta:\n        verbose_name = _(\"Sale\")\n        verbose_name_plural = _(\"Sales\")\n        ordering = [\"-created_at\"]\n\n\nclass SalesProductWithCustomID(models.Model):\n    slug = models.SlugField()\n    doc_date = models.DateTimeField(_(\"date\"), db_index=True)\n    client = models.ForeignKey(Client, on_delete=models.CASCADE)\n    product = models.ForeignKey(ProductCustomID, on_delete=models.CASCADE)\n    quantity = models.DecimalField(\n        _(\"quantity\"), max_digits=19, decimal_places=2, default=0\n    )\n    price = models.DecimalField(_(\"price\"), max_digits=19, decimal_places=2, default=0)\n    value = models.DecimalField(_(\"value\"), max_digits=19, decimal_places=2, default=0)\n    created_at = models.DateTimeField(null=True, verbose_name=_(\"Created at\"))\n    flag = models.CharField(max_length=50, default=\"sales\")\n\n    content_type = models.ForeignKey(\n        ContentType, on_delete=models.DO_NOTHING, null=True\n    )\n    object_id = models.PositiveIntegerField(null=True)\n    content_object = GenericForeignKey(\"content_type\", \"object_id\")\n\n    def save(\n            self, *args, **kwargs\n    ):\n        self.value = self.quantity * self.price\n        super().save(*args, **kwargs)\n\n    class Meta:\n        verbose_name = _(\"Sale\")\n        verbose_name_plural = _(\"Sales\")\n        ordering = [\"-created_at\"]\n\n\nclass SalesWithFlag(models.Model):\n    slug = models.SlugField()\n    doc_date = models.DateTimeField(_(\"date\"), db_index=True)\n    client = models.ForeignKey(Client, on_delete=models.CASCADE)\n    product = models.ForeignKey(Product, on_delete=models.CASCADE)\n    quantity = models.DecimalField(\n        _(\"quantity\"), max_digits=19, decimal_places=2, default=0\n    )\n    price = models.DecimalField(_(\"price\"), max_digits=19, decimal_places=2, default=0)\n    value = models.DecimalField(_(\"value\"), max_digits=19, decimal_places=2, default=0)\n    created_at = models.DateTimeField(null=True, verbose_name=_(\"Created at\"))\n    flag = models.CharField(max_length=50, default=\"sales\")\n\n    def save(\n            self, *args, **kwargs\n    ):\n        self.value = self.quantity * self.price\n        super().save(*args, **kwargs)\n\n    class Meta:\n        verbose_name = _(\"Sale\")\n        verbose_name_plural = _(\"Sales\")\n        ordering = [\"-created_at\"]\n\n\nclass UserJoined(models.Model):\n    username = models.CharField(max_length=255)\n    date_joined = models.DateField()\n\n\nclass TaxCode(models.Model):\n    name = models.CharField(max_length=255)\n    tax = models.DecimalField(_(\"tax\"), max_digits=19, decimal_places=2, default=0)\n\n\nclass ComplexSales(models.Model):\n    tax = models.ManyToManyField(TaxCode)\n    slug = models.SlugField()\n    doc_date = models.DateTimeField(_(\"date\"), db_index=True)\n    client = models.ForeignKey(Client, on_delete=models.CASCADE)\n    product = models.ForeignKey(Product, on_delete=models.CASCADE)\n    quantity = models.DecimalField(\n        _(\"quantity\"), max_digits=19, decimal_places=2, default=0\n    )\n    price = models.DecimalField(_(\"price\"), max_digits=19, decimal_places=2, default=0)\n    value = models.DecimalField(_(\"value\"), max_digits=19, decimal_places=2, default=0)\n    created_at = models.DateTimeField(null=True, verbose_name=_(\"Created at\"))\n    flag = models.CharField(max_length=50, default=\"sales\")\n\n    content_type = models.ForeignKey(\n        ContentType, on_delete=models.DO_NOTHING, null=True\n    )\n    object_id = models.PositiveIntegerField(null=True)\n    content_object = GenericForeignKey(\"content_type\", \"object_id\")\n\n    def save(\n            self, *args, **kwargs\n    ):\n        self.value = self.quantity * self.price\n        super().save(*args, **kwargs)\n\n    class Meta:\n        verbose_name = _(\"VAT Sale\")\n        verbose_name_plural = _(\"VAT Sales\")\n        ordering = [\"-created_at\"]\n\n\n#\n# class Invoice(BaseMovementInfo):\n#     client = models.ForeignKey(Client, on_delete=models.CASCADE)\n#\n#     @classmethod\n#     def get_doc_type(cls):\n#         return 'sales'\n#\n#\n# class InvoiceLine(QuanValueMovementItem):\n#     invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)\n#\n#     product = models.ForeignKey(Product, on_delete=models.CASCADE)\n#     client = models.ForeignKey(Client, on_delete=models.CASCADE)\n#\n#     @classmethod\n#     def get_doc_type(cls):\n#         return 'sales'\n#\n#\n# class Journal(BaseMovementInfo):\n#     data = models.CharField(max_length=100, null=True, blank=True)\n#\n#     @classmethod\n#     def get_doc_type(cls):\n#         return 'journal-sales'\n#\n#\n# class JournalItem(BaseMovementItemInfo):\n#     journal = models.ForeignKey(Journal, on_delete=models.CASCADE)\n#     client = models.ForeignKey(Client, on_delete=models.CASCADE)\n#     data = models.CharField(max_length=100, null=True, blank=True)\n#\n#     @classmethod\n#     def get_doc_type(cls):\n#         return 'journal-sales'\n#\n#\n# class JournalWithCriteria(Journal):\n#     class Meta:\n#         proxy = True\n#\n\n# Vanilla models\n\n\nclass Order(models.Model):\n    date_placed = models.DateTimeField(auto_created=True)\n    client = models.ForeignKey(Client, null=True, on_delete=models.CASCADE)\n\n\nclass OrderLine(models.Model):\n    date_placed = models.DateTimeField(auto_created=True)\n    product = models.ForeignKey(Product, on_delete=models.CASCADE)\n    order = models.ForeignKey(Order, on_delete=models.CASCADE)\n    quantity = models.PositiveIntegerField(default=0)\n    client = models.ForeignKey(Client, null=True, on_delete=models.CASCADE)\n\n\nclass Architect(models.Model):\n    \"\"\"A lookup table for CX Enterprise Architects, used mostly for reporting purposes. Associated with Initiatives and Features.\"\"\"\n\n    id = models.AutoField(primary_key=True)\n    name = models.CharField(\n        max_length=60,\n        verbose_name=\"Lead Architect\",\n        null=False,\n        unique=True,\n        blank=False,\n        db_index=True,\n    )\n\n\nclass Initiative(models.Model):\n    id = models.AutoField(primary_key=True)\n    # cx_pem = models.ForeignKey(ProjectEngineeringManager, on_delete=models.DO_NOTHING,\n    #                            verbose_name=\"CX PEM:\", null=True,\n    #                            blank=True)\n    architect = models.ForeignKey(\n        Architect,\n        on_delete=models.DO_NOTHING,\n        verbose_name=\"CX Architect:\",\n        null=True,\n        blank=True,\n        to_field=\"name\",\n    )\n"
  },
  {
    "path": "tests/report_generators.py",
    "content": "from __future__ import annotations\n\nimport datetime\n\nfrom django.db.models import Sum, Count\nfrom django.utils.translation import gettext_lazy as _\n\nfrom slick_reporting.fields import ComputationField, PercentageToTotalBalance\nfrom slick_reporting.generator import ReportGenerator\nfrom .models import (\n    Client,\n    SimpleSales,\n    Product,\n    SalesWithFlag,\n    SalesProductWithCustomID,\n    ComplexSales,\n    SimpleSales2,\n)\nfrom .models import OrderLine\n\n\nclass GenericGenerator(ReportGenerator):\n    report_model = OrderLine\n    date_field = \"order__date_placed\"\n\n    # here is the meat and potatos of the report,\n    # we group the sales per client , we display columns slug and title (of the `base_model` defied above\n    # and we add the magic field `__balance__` we compute the client balance.\n    group_by = \"client\"\n    columns = [\"slug\", \"name\"]\n\n\nclass GeneratorWithAttrAsColumn(GenericGenerator):\n    group_by = \"client\"\n\n    columns = [\"get_data\", \"slug\", \"name\"]\n\n    def get_data(self, obj):\n        return obj[\"name\"]\n\n    get_data.verbose_name = \"My Verbose Name\"\n\n\nclass CrosstabOnClient(GenericGenerator):\n    group_by = \"product\"\n    columns = [\"name\", \"__total_quantity__\"]\n    crosstab_field = \"client\"\n    # crosstab_columns = ['__total_quantity__']\n    crosstab_columns = [ComputationField.create(Sum, \"quantity\", name=\"value__sum\", verbose_name=_(\"Sales\"))]\n\n\nclass CrosstabTimeSeries(GenericGenerator):\n    group_by = \"product\"\n    columns = [\"name\", \"__total_quantity__\"]\n    # crosstab_field = \"client\"\n    # crosstab_columns = [\n    #     ComputationField.create(\n    #         Sum, \"quantity\", name=\"value__sum\", verbose_name=_(\"Sales\")\n    #     )\n    # ]\n    # crosstab_compute_remainder = False\n\n    # time_series_pattern = \"monthly\"\n    # time_series_columns = [\"__total_quantity__\"]\n\n\nclass CrosstabOnField(ReportGenerator):\n    report_model = ComplexSales\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"name\"]\n    crosstab_field = \"flag\"\n    crosstab_ids = [\"sales\", \"sales-return\"]\n\n    crosstab_columns = [ComputationField.create(Sum, \"quantity\", name=\"value__sum\", verbose_name=_(\"Sales\"))]\n\n\nclass CrosstabCustomQueryset(ReportGenerator):\n    report_model = ComplexSales\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"name\"]\n    crosstab_field = \"flag\"\n    # crosstab_ids = [\"sales\", \"sales-return\"]\n\n    crosstab_ids_custom_filters = [\n        (None, dict(flag=\"sales\")),\n        (None, dict(flag=\"sales-return\")),\n    ]\n\n    crosstab_columns = [ComputationField.create(Sum, \"quantity\", name=\"value__sum\", verbose_name=_(\"Sales\"))]\n\n\nclass CrosstabOnTraversingField(ReportGenerator):\n    report_model = ComplexSales\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"name\"]\n\n    crosstab_field = \"client__sex\"\n    crosstab_ids = [\"FEMALE\", \"MALE\", \"OTHER\"]\n\n    crosstab_columns = [ComputationField.create(Sum, \"quantity\", name=\"value__sum\", verbose_name=_(\"Sales\"))]\n\n\nclass ClientTotalBalance(ReportGenerator):\n    report_model = SimpleSales\n    # date_field = \"doc_date\"\n    group_by = \"client\"\n    columns = [\n        \"slug\",\n        \"name\",\n        \"__balance__\",\n        ComputationField.create(Sum, \"value\", name=\"__total__\", verbose_name=_(\"Sales\")),\n    ]\n\n\nclass TotalBalanceWithQueryset(ReportGenerator):\n    report_model = SimpleSales\n    queryset = SimpleSales.objects.filter(product_id=0)\n    date_field = \"doc_date\"\n    group_by = \"client\"\n    columns = [\"slug\", \"name\", \"__balance__\", \"__total__\"]\n\n\nclass ClientTotalBalance2(ReportGenerator):\n    report_model = SimpleSales2\n    date_field = \"doc_date\"\n    group_by = \"client\"\n    columns = [\"slug\", \"name\", \"__balance__\", \"__total__\"]\n\n\nclass GroupByCharField(ReportGenerator):\n    report_model = SalesWithFlag\n    date_field = \"doc_date\"\n    group_by = \"flag\"\n    columns = [\"flag\", \"__balance__\", ComputationField.create(Sum, \"quantity\")]\n\n\nclass GroupByCharFieldPlusTimeSeries(ReportGenerator):\n    report_model = SalesWithFlag\n    date_field = \"doc_date\"\n    group_by = \"flag\"\n    columns = [\"flag\", ComputationField.create(Sum, \"quantity\")]\n\n    time_series_pattern = \"monthly\"\n    time_series_columns = [ComputationField.create(Sum, \"quantity\")]\n\n\nclass ClientTotalBalancesOrdered(ClientTotalBalance):\n    report_slug = None\n    default_order_by = \"__balance__\"\n\n\nclass ClientTotalBalancesOrderedDESC(ClientTotalBalance):\n    report_slug = None\n    default_order_by = \"-__balance__\"\n\n\nclass ProductTotalSales(ReportGenerator):\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n    group_by = \"product\"\n    columns = [\n        \"slug\",\n        \"name\",\n        \"__balance__\",\n        \"__balance_quantity__\",\n        \"get_object_sku\",\n        \"average_value\",\n    ]\n\n    def get_object_sku(self, obj: dict, row: dict) -> any:\n        \"\"\"\n        :param obj: obj is the current row of the grouped by model , or the current row of the queryset\n        :param row: the current report row values in a dictionary\n        :return:\n        \"\"\"\n        return obj[\"sku\"].upper()\n\n    get_object_sku.verbose_name = \"SKU ALL CAPS\"\n\n    def average_value(self, obj, data):\n        return data[\"__balance__\"] / data[\"__balance_quantity__\"]\n\n    average_value.verbose_name = \"Average Value\"\n\n\nclass ProductTotalSalesProductWithCustomID(ReportGenerator):\n    report_model = SalesProductWithCustomID\n    date_field = \"doc_date\"\n    group_by = \"product\"\n    columns = [\"slug\", \"name\", \"__balance__\", \"__balance_quantity__\"]\n\n\nclass ProductTotalSalesWithPercentage(ReportGenerator):\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n    group_by = \"client\"\n    columns = [\n        \"slug\",\n        \"name\",\n        \"__balance__\",\n        \"__balance_quantity__\",\n        PercentageToTotalBalance,\n    ]\n\n\nclass ClientList(ReportGenerator):\n    report_model = SimpleSales\n\n    group_by = \"client\"\n    columns = [\"slug\", \"name\"]\n\n\nclass ProductClientSales(ReportGenerator):\n    report_model = SimpleSales\n\n    report_slug = \"client_sales_of_products\"\n    report_title = _(\"Client net sales for each product\")\n    must_exist_filter = \"client_id\"\n    header_report = ClientList\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\", \"__balance_quantity__\", \"__balance__\", \"get_data\"]\n\n    def get_data(self, obj):\n        return \"\"\n\n\nclass ProductSalesMonthlySeries(ReportGenerator):\n    base_model = Product\n    report_model = SimpleSales\n    report_title = _(\"Product Sales Monthly\")\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n    time_series_pattern = (\"monthly\",)\n    time_series_columns = [\"__balance_quantity__\", \"__balance__\"]\n\n    chart_settings = [\n        {\n            \"id\": \"movement_column\",\n            \"name\": _(\"comparison - column\"),\n            \"settings\": {\n                \"chart_type\": \"column\",\n                \"name\": _(\"{product} Avg. purchase price \"),\n                \"sub_title\": _(\"{date_verbose}\"),\n                \"y_sources\": [\"__balance__\"],\n                \"series_names\": [_(\"Avg. purchase price\")],\n            },\n        },\n        {\n            \"id\": \"movement_line\",\n            \"name\": _(\"comparison - line\"),\n            \"settings\": {\n                \"chart_type\": \"line\",\n                \"name\": _(\"{product} Avg. purchase price \"),\n                \"sub_title\": _(\"{date_verbose}\"),\n                \"y_sources\": [\"__balance__\"],\n                \"series_names\": [_(\"Avg. purchase price\")],\n            },\n        },\n    ]\n\n\nclass TimeSeriesCustomDates(ReportGenerator):\n    report_model = SimpleSales\n    report_title = _(\"Product Sales Monthly\")\n    date_field = \"doc_date\"\n    # group_by = 'product'\n    # columns = ['slug', 'name']\n    time_series_pattern = \"custom\"\n    time_series_columns = [\"__total__\"]\n    time_series_custom_dates = [\n        (datetime.date(2020, 1, 1), datetime.date(2020, 1, 17)),\n        (datetime.date(2020, 4, 17), datetime.date(2020, 5, 1)),\n        (datetime.date(2020, 8, 8), datetime.date(2020, 9, 9)),\n    ]\n\n\nclass TimeSeriesWithOutGroupBy(ReportGenerator):\n    report_model = SimpleSales\n    report_title = _(\"Product Sales Monthly\")\n    date_field = \"doc_date\"\n    # group_by = 'product'\n    # columns = ['slug', 'name']\n    time_series_pattern = \"monthly\"\n    time_series_columns = [\"__total__\"]\n\n\nclass ClientReportMixin:\n    base_model = Client\n    report_model = SimpleSales\n\n\nclass ClientSalesMonthlySeries(ReportGenerator):\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n\n    group_by = \"client\"\n    columns = [\"slug\", \"name\"]\n    time_series_pattern = \"monthly\"\n    time_series_columns = [\"__debit__\", \"__credit__\", \"__balance__\", \"__total__\"]\n\n\nclass CountField(ComputationField):\n    calculation_field = \"id\"\n    calculation_method = Count\n    verbose_name = _(\"Count\")\n    name = \"count__id\"\n\n\nclass TestCountField(ReportGenerator):\n    report_model = ComplexSales\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\", CountField]\n    date_field = \"doc_date\"\n\n\n#\n\n\nclass ClientDetailedStatement(ReportGenerator):\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n    group_by = None\n    columns = [\"slug\", \"doc_date\", \"product__name\", \"quantity\", \"price\", \"value\"]\n\n\nclass ClientDetailedStatement2(ReportGenerator):\n    report_title = _(\"client statement\")\n    base_model = Client\n    report_model = SimpleSales\n\n    header_report = ClientList\n    must_exist_filter = \"client_id\"\n\n    form_settings = {\n        \"group_by\": \"\",\n        \"group_columns\": [\n            \"slug\",\n            \"doc_date\",\n            \"doc_type\",\n            \"product__title\",\n            \"quantity\",\n            \"price\",\n            \"value\",\n        ],\n    }\n    group_by = None\n    columns = [\n        \"slug\",\n        \"doc_date\",\n        \"doc_type\",\n        \"product__title\",\n        \"quantity\",\n        \"price\",\n        \"value\",\n    ]\n\n\nclass ProductClientSalesMatrix(ReportGenerator):\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n\n    crosstab_field = \"client\"\n    crosstab_columns = [\"__total__\"]\n\n\nclass ProductClientSalesMatrixToFieldSet(ReportGenerator):\n    report_model = SimpleSales2\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n\n    crosstab_field = \"client\"\n    crosstab_columns = [\"__total__\"]\n\n\nclass ProductClientSalesMatrix2(ReportGenerator):\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n\n    crosstab_field = \"client\"\n    crosstab_columns = [ComputationField.create(Sum, \"value\", name=\"value__sum\", verbose_name=_(\"Sales\"))]\n\n\nclass ProductClientSalesMatrixwSimpleSales2(ReportGenerator):\n    report_model = SimpleSales2\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n\n    crosstab_field = \"client\"\n    crosstab_columns = [ComputationField.create(Sum, \"value\", name=\"value__sum\", verbose_name=_(\"Sales\"))]\n\n\nclass GeneratorClassWithAttrsAs(ReportGenerator):\n    columns = [\"get_icon\", \"slug\", \"name\"]\n\n\nclass ClientTotalBalancesWithShowEmptyFalse(ClientTotalBalance):\n    report_slug = None\n    default_order_by = \"-__balance__\"\n    show_empty_records = False\n"
  },
  {
    "path": "tests/requirements.txt",
    "content": "-r ../requirements.txt\ncrispy-bootstrap4"
  },
  {
    "path": "tests/settings.py",
    "content": "import os\n\nSECRET_KEY = \"fake-key\"\n\nPROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\nBASE_DIR = os.path.dirname(PROJECT_DIR)\n\nDATABASES = {\n    \"default\": {\n        \"ENGINE\": \"django.db.backends.sqlite3\",\n        \"NAME\": os.path.join(BASE_DIR, \"db.sqlite3\"),\n        \"TEST\": {\"NAME\": \"tst_db.sqlite3\", \"MIGRATE\": False},\n    },\n}\n\nPASSWORD_HASHERS = [\n    \"django.contrib.auth.hashers.MD5PasswordHasher\",\n]\n\nINSTALLED_APPS = [\n    # 'django.contrib.admin',\n    \"django.contrib.contenttypes\",\n    \"django.contrib.auth\",\n    \"django.contrib.sites\",\n    \"django.contrib.sessions\",\n    \"django.contrib.messages\",\n    # 'django.contrib.admin.apps.SimpleAdminConfig',\n    \"django.contrib.staticfiles\",\n    \"slick_reporting\",\n    \"crispy_forms\",\n    \"crispy_bootstrap4\",\n    \"tests\",\n]\n\nROOT_URLCONF = \"tests.urls\"\n\nTEMPLATES = [\n    {\n        \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n        \"DIRS\": [],\n        \"APP_DIRS\": True,\n        \"OPTIONS\": {\n            \"context_processors\": [\n                \"django.template.context_processors.debug\",\n                \"django.template.context_processors.request\",\n                \"django.contrib.auth.context_processors.auth\",\n                \"django.contrib.messages.context_processors.messages\",\n                \"django.template.context_processors.static\",\n            ],\n        },\n    },\n]\nSTATIC_URL = \"/static/\"\n\nMIGRATION_MODULES = {\"contenttypes\": None, \"auth\": None}\nDEFAULT_AUTO_FIELD = \"django.db.models.BigAutoField\"\n\nCRISPY_TEMPLATE_PACK = \"bootstrap4\"\n\n\nMIDDLEWARE = [\n    \"django.middleware.security.SecurityMiddleware\",\n    \"django.contrib.sessions.middleware.SessionMiddleware\",\n    \"django.middleware.common.CommonMiddleware\",\n    \"django.middleware.csrf.CsrfViewMiddleware\",\n    \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n]\n"
  },
  {
    "path": "tests/templates/base.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Title</title>\n    {% block content %}\n    {% endblock %}\n</head>\n<body>\n\n</body>\n</html>"
  },
  {
    "path": "tests/test_dynamic_model.py",
    "content": "import datetime\n\nfrom django.db import connection, models\nfrom django.db.models import Sum, Count\nfrom django.test import TestCase\n\nfrom slick_reporting.dynamic_model import get_dynamic_model, _model_cache\nfrom slick_reporting.fields import ComputationField\nfrom slick_reporting.generator import ReportGenerator\n\n\nTABLE_NAME = \"test_dynamic_sales\"\n\nCREATE_TABLE_SQL = f\"\"\"\n    CREATE TABLE {TABLE_NAME} (\n        id INTEGER PRIMARY KEY AUTOINCREMENT,\n        product_name VARCHAR(100) NOT NULL,\n        client_name VARCHAR(100),\n        doc_date DATE NOT NULL,\n        quantity INTEGER NOT NULL DEFAULT 0,\n        price DECIMAL(10, 2) NOT NULL DEFAULT 0,\n        value DECIMAL(10, 2) NOT NULL DEFAULT 0\n    )\n\"\"\"\n\nINSERT_SQL = f\"\"\"\n    INSERT INTO {TABLE_NAME} (product_name, client_name, doc_date, quantity, price, value)\n    VALUES (?, ?, ?, ?, ?, ?)\n\"\"\"\n\n\nclass DynamicModelTestBase(TestCase):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        with connection.cursor() as cursor:\n            cursor.execute(CREATE_TABLE_SQL)\n            rows = [\n                (\"Product A\", \"Client 1\", \"2024-01-15\", 10, 5.00, 50.00),\n                (\"Product A\", \"Client 1\", \"2024-02-15\", 5, 5.00, 25.00),\n                (\"Product A\", \"Client 2\", \"2024-01-20\", 3, 5.00, 15.00),\n                (\"Product B\", \"Client 1\", \"2024-01-10\", 7, 10.00, 70.00),\n                (\"Product B\", \"Client 2\", \"2024-02-10\", 2, 10.00, 20.00),\n                (\"Product C\", \"Client 2\", \"2024-03-01\", 1, 20.00, 20.00),\n            ]\n            cursor.executemany(INSERT_SQL, rows)\n\n    @classmethod\n    def tearDownClass(cls):\n        with connection.cursor() as cursor:\n            cursor.execute(f\"DROP TABLE IF EXISTS {TABLE_NAME}\")\n        # Clean up model cache and app registry\n        keys_to_remove = [k for k in _model_cache if k.endswith(f\":{TABLE_NAME}\")]\n        for k in keys_to_remove:\n            del _model_cache[k]\n        from django.apps import apps\n\n        try:\n            del apps.all_models[\"slick_reporting\"][\"testdynamicsales\"]\n        except KeyError:\n            pass\n        super().tearDownClass()\n\n\nclass TestGetDynamicModel(DynamicModelTestBase):\n    def test_returns_model_class(self):\n        model = get_dynamic_model(TABLE_NAME)\n        self.assertTrue(issubclass(model, models.Model))\n\n    def test_model_meta(self):\n        model = get_dynamic_model(TABLE_NAME)\n        self.assertEqual(model._meta.db_table, TABLE_NAME)\n        self.assertFalse(model._meta.managed)\n\n    def test_model_fields(self):\n        model = get_dynamic_model(TABLE_NAME)\n        field_names = {f.name for f in model._meta.get_fields()}\n        self.assertIn(\"id\", field_names)\n        self.assertIn(\"product_name\", field_names)\n        self.assertIn(\"client_name\", field_names)\n        self.assertIn(\"doc_date\", field_names)\n        self.assertIn(\"quantity\", field_names)\n        self.assertIn(\"price\", field_names)\n        self.assertIn(\"value\", field_names)\n\n    def test_pk_field(self):\n        model = get_dynamic_model(TABLE_NAME)\n        pk_field = model._meta.pk\n        self.assertIsNotNone(pk_field)\n        self.assertEqual(pk_field.name, \"id\")\n\n    def test_cache_returns_same_model(self):\n        model1 = get_dynamic_model(TABLE_NAME)\n        model2 = get_dynamic_model(TABLE_NAME)\n        self.assertIs(model1, model2)\n\n    def test_nonexistent_table_raises(self):\n        with self.assertRaises(ValueError) as cm:\n            get_dynamic_model(\"nonexistent_table_xyz\")\n        self.assertIn(\"nonexistent_table_xyz\", str(cm.exception))\n\n\nclass TestDynamicModelQuerySet(DynamicModelTestBase):\n    def test_objects_all(self):\n        model = get_dynamic_model(TABLE_NAME)\n        qs = model.objects.all()\n        self.assertEqual(qs.count(), 6)\n\n    def test_filter(self):\n        model = get_dynamic_model(TABLE_NAME)\n        qs = model.objects.filter(product_name=\"Product A\")\n        self.assertEqual(qs.count(), 3)\n\n    def test_values(self):\n        model = get_dynamic_model(TABLE_NAME)\n        products = list(\n            model.objects.values(\"product_name\").distinct().order_by(\"product_name\")\n        )\n        self.assertEqual(len(products), 3)\n        self.assertEqual(products[0][\"product_name\"], \"Product A\")\n\n    def test_aggregate(self):\n        model = get_dynamic_model(TABLE_NAME)\n        result = model.objects.aggregate(total=Sum(\"value\"))\n        self.assertEqual(result[\"total\"], 200.00)\n\n    def test_annotate(self):\n        model = get_dynamic_model(TABLE_NAME)\n        result = list(\n            model.objects.values(\"product_name\")\n            .annotate(total_value=Sum(\"value\"))\n            .order_by(\"product_name\")\n        )\n        self.assertEqual(result[0][\"product_name\"], \"Product A\")\n        self.assertEqual(result[0][\"total_value\"], 90.00)\n\n\nclass TestReportGeneratorWithDynamicModel(DynamicModelTestBase):\n    def test_group_by_report(self):\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_name\",\n            date_field=\"doc_date\",\n            columns=[\n                \"product_name\",\n                ComputationField.create(Sum, \"value\", name=\"value__sum\", verbose_name=\"Total Value\"),\n            ],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        data = report.get_report_data()\n        self.assertEqual(len(data), 3)\n        values_by_product = {row[\"product_name\"]: row[\"value__sum\"] for row in data}\n        self.assertEqual(values_by_product[\"Product A\"], 90.00)\n        self.assertEqual(values_by_product[\"Product B\"], 90.00)\n        self.assertEqual(values_by_product[\"Product C\"], 20.00)\n\n    def test_time_series_report(self):\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_name\",\n            date_field=\"doc_date\",\n            columns=[\"product_name\", \"__time_series__\"],\n            time_series_pattern=\"monthly\",\n            time_series_columns=[\n                ComputationField.create(Sum, \"value\", name=\"ts_value\", verbose_name=\"Value\"),\n            ],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 3, 31),\n        )\n        data = report.get_report_data()\n        self.assertEqual(len(data), 3)\n\n    def test_table_name_convenience_param(self):\n        report = ReportGenerator(\n            table_name=TABLE_NAME,\n            group_by=\"product_name\",\n            date_field=\"doc_date\",\n            columns=[\n                \"product_name\",\n                ComputationField.create(Sum, \"value\", name=\"value__sum2\", verbose_name=\"Total\"),\n            ],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        data = report.get_report_data()\n        self.assertEqual(len(data), 3)\n\n    def test_no_group_by_report(self):\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"\",\n            date_field=\"doc_date\",\n            columns=[\n                ComputationField.create(Sum, \"value\", name=\"total_val\", verbose_name=\"Total\"),\n            ],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        data = report.get_report_data()\n        self.assertEqual(data[0][\"total_val\"], 200.00)\n\n\nclass TestReportViewTableNameImportSafety(TestCase):\n    \"\"\"Defining a ReportView with table_name must not touch the DB at class-definition time.\"\"\"\n\n    def test_class_definition_does_not_hit_db(self):\n        from slick_reporting.views import ReportView\n\n        try:\n\n            class _GhostTableReport(ReportView):\n                table_name = \"nonexistent_table_xyz\"\n                date_field = \"doc_date\"\n                group_by = \"product_name\"\n                columns = [\n                    \"product_name\",\n                    ComputationField.create(Sum, \"value\", name=\"v__sum\", verbose_name=\"V\"),\n                ]\n\n        except Exception as exc:\n            self.fail(\n                f\"Defining a ReportView with table_name must not hit the DB at \"\n                f\"class-definition time, but got: {type(exc).__name__}: {exc}\"\n            )\n"
  },
  {
    "path": "tests/test_generator.py",
    "content": "from datetime import datetime\n\nfrom django.db.models import Sum\nfrom django.test import TestCase\nfrom django.utils.translation import gettext_lazy as _\n\nfrom slick_reporting.fields import ComputationField\nfrom slick_reporting.generator import ReportGenerator, ListViewReportGenerator\nfrom slick_reporting.helpers import get_foreign_keys\nfrom .models import OrderLine, ComplexSales\nfrom .models import SimpleSales, Client\nfrom .report_generators import (\n    GeneratorWithAttrAsColumn,\n    CrosstabOnClient,\n    GenericGenerator,\n    GroupByCharField,\n    TimeSeriesCustomDates,\n    CrosstabOnField,\n    CrosstabOnTraversingField,\n    CrosstabCustomQueryset,\n    TestCountField,\n)\nfrom .tests import BaseTestData, year\n\n\nclass CrosstabTests(BaseTestData, TestCase):\n    def test_matrix_column_included(self):\n        report = CrosstabOnClient(crosstab_ids=[self.client1.pk], crosstab_compute_remainder=False)\n        columns = report.get_list_display_columns()\n        self.assertEqual(len(columns), 3, columns)\n\n        report = CrosstabOnClient(crosstab_ids=[self.client1.pk], crosstab_compute_remainder=True)\n        columns = report.get_list_display_columns()\n        self.assertEqual(len(columns), 4, columns)\n\n    def test_matrix_column_position(self):\n        report = CrosstabOnClient(\n            columns=[\"__crosstab__\", \"name\", \"__total_quantity__\"],\n            crosstab_ids=[self.client1.pk],\n            crosstab_compute_remainder=False,\n        )\n        columns = report.get_list_display_columns()\n        self.assertEqual(len(columns), 3, columns)\n        self.assertEqual(columns[0][\"name\"], \"value__sumCT1\")\n\n        report = CrosstabOnClient(crosstab_ids=[self.client1.pk], crosstab_compute_remainder=True)\n        columns = report.get_list_display_columns()\n        self.assertEqual(len(columns), 4, columns)\n\n    def test_get_crosstab_columns(self):\n        report = CrosstabOnClient(crosstab_ids=[self.client1.pk])\n        columns = report.get_list_display_columns()\n        self.assertEqual(len(columns), 4)\n\n        report = CrosstabOnClient(\n            crosstab_ids=[self.client1.pk, self.client2.pk],\n            crosstab_columns=[\"__total_quantity__\", \"__balance_quantity__\"],\n        )\n        columns = report.get_list_display_columns()\n        self.assertEqual(len(columns), 8, [x[\"name\"] for x in columns])\n\n    def test_get_crosstab_parsed_columns(self):\n        \"\"\"\n        Test important attributes are passed .\n        :return:\n        \"\"\"\n        report = CrosstabOnClient(crosstab_ids=[self.client1.pk], crosstab_compute_remainder=False)\n        columns = report.get_crosstab_parsed_columns()\n        for col in columns:\n            self.assertTrue(\"is_summable\" in col.keys(), col)\n\n    def test_crosstab_on_field(self):\n        report = CrosstabOnField()\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2, data)\n        self.assertEqual(data[0][\"value__sumCTsales\"], 90, data)\n        self.assertEqual(data[0][\"value__sumCTsales-return\"], 30, data)\n        self.assertEqual(data[0][\"value__sumCT----\"], 77, data)\n        self.assertEqual(data[1][\"value__sumCTsales-return\"], 34, data)\n\n    def test_crosstab_ids_queryset(self):\n        # same test values as above, tests that crosstab_ids_custom_filters\n        report = CrosstabCustomQueryset()\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2, data)\n        self.assertEqual(data[0][\"value__sumCT0\"], 90, data)\n        self.assertEqual(data[0][\"value__sumCT1\"], 30, data)\n        self.assertEqual(data[1][\"value__sumCT1\"], 34, data)\n\n    def test_crosstab_on_traversing_field(self):\n        report = CrosstabOnTraversingField()\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2, data)\n        self.assertEqual(data[0][\"value__sumCTOTHER\"], 120, data)\n        self.assertEqual(data[0][\"value__sumCTFEMALE\"], 77, data)\n        self.assertEqual(data[0][\"value__sumCT----\"], 0, data)\n        self.assertEqual(data[1][\"value__sumCTOTHER\"], 34, data)\n\n    def test_crosstab_time_series(self):\n        report = ReportGenerator(\n            report_model=ComplexSales,\n            date_field=\"doc_date\",\n            group_by=\"product\",\n            columns=[\"name\", \"__total_quantity__\"],\n            time_series_pattern=\"monthly\",\n            crosstab_field=\"client\",\n            crosstab_columns=[ComputationField.create(Sum, \"quantity\", name=\"value__sum\", verbose_name=_(\"Sales\"))],\n            crosstab_ids=[self.client2.pk, self.client3.pk],\n            crosstab_compute_remainder=False,\n        )\n        columns = report.get_list_display_columns()\n        time_series_columns = report.get_time_series_parsed_columns()\n        expected_num_of_columns = 2 * datetime.today().month  # 2 client + 1 remainder * months since start of year\n\n        self.assertEqual(len(time_series_columns), expected_num_of_columns, columns)\n        data = report.get_report_data()\n        self.assertEqual(data[0][\"__total_quantity__\"], 197, data)\n        sum_o_product_1 = 0\n        for col in data[0]:\n            if col.startswith(\"value__\") and \"TS\" in col:\n                sum_o_product_1 += data[0][col]\n\n        self.assertEqual(sum_o_product_1, 197, data)\n\n\nclass GeneratorReportStructureTest(BaseTestData, TestCase):\n    @classmethod\n    def setUpTestData(cls):\n        super().setUpTestData()\n        SimpleSales.objects.create(\n            doc_date=datetime(year, 3, 2),\n            client=cls.client3,\n            product=cls.product3,\n            quantity=30,\n            price=10,\n        )\n\n    def test_time_series_columns_inclusion(self):\n        x = ReportGenerator(\n            OrderLine,\n            date_field=\"order__date_placed\",\n            group_by=\"client\",\n            columns=[\"name\", \"__time_series__\"],\n            time_series_columns=[\"__total_quantity__\"],\n            time_series_pattern=\"monthly\",\n            start_date=datetime(2020, 1, 1),\n            end_date=datetime(2020, 12, 31),\n        )\n        self.assertEqual(len(x.get_list_display_columns()), 13)\n\n    def test_time_series_patterns(self):\n        from slick_reporting.fields import TotalReportField\n\n        report = ReportGenerator(\n            OrderLine,\n            date_field=\"order__date_placed\",\n            group_by=\"client\",\n            columns=[\"name\", \"__time_series__\"],\n            time_series_columns=[\"__total_quantity__\"],\n            time_series_pattern=\"monthly\",\n            start_date=datetime(2020, 1, 1),\n            end_date=datetime(2020, 12, 31),\n        )\n\n        dates = report._get_time_series_dates()\n        self.assertEqual(len(dates), 12)\n        self.assertIsNotNone(report.get_time_series_field_verbose_name(TotalReportField, dates[0], 0, dates))\n\n        dates = report._get_time_series_dates(\"daily\")\n        self.assertEqual(len(dates), 365, len(dates))\n        self.assertIsNotNone(report.get_time_series_field_verbose_name(TotalReportField, dates[0], 0, dates, \"daily\"))\n\n        dates = report._get_time_series_dates(\"weekly\")\n        self.assertEqual(len(dates), 53, len(dates))\n        self.assertIsNotNone(report.get_time_series_field_verbose_name(TotalReportField, dates[0], 0, dates, \"weekly\"))\n\n        dates = report._get_time_series_dates(\"bi-weekly\")\n        self.assertEqual(len(dates), 27, len(dates))\n        self.assertIsNotNone(\n            report.get_time_series_field_verbose_name(TotalReportField, dates[0], 0, dates, \"semimonthly\")\n        )\n\n        dates = report._get_time_series_dates(\"quarterly\")\n        self.assertEqual(len(dates), 4, len(dates))\n\n        dates = report._get_time_series_dates(\"semiannually\")\n        self.assertEqual(len(dates), 2, len(dates))\n        dates = report._get_time_series_dates(\"annually\")\n        self.assertEqual(len(dates), 1, len(dates))\n        self.assertIsNotNone(report.get_time_series_field_verbose_name(TotalReportField, dates[0], 0, dates))\n\n        def not_known_pattern():\n            report._get_time_series_dates(\"each_spring\")\n\n        self.assertRaises(Exception, not_known_pattern)\n\n    def test_time_series_custom_pattern(self):\n        # report = ReportGenerator(OrderLine, date_field='order__date_placed', group_by='client',\n        #                          columns=['name', '__time_series__'],\n        #                          time_series_columns=['__total_quantity__'], time_series_pattern='monthly',\n        #                          start_date=datetime(2020, 1, 1, tzinfo=pytz.timezone('utc')),\n        #                          end_date=datetime(2020, 12, 31, tzinfo=pytz.timezone('utc')))\n        report = TimeSeriesCustomDates()\n        dates = report._get_time_series_dates()\n        self.assertEqual(len(dates), 3, dates)\n\n    def test_time_series_columns_placeholder(self):\n        x = ReportGenerator(\n            OrderLine,\n            date_field=\"order__date_placed\",\n            group_by=\"client\",\n            columns=[\"name\"],\n            time_series_columns=[\"__total_quantity__\"],\n            time_series_pattern=\"monthly\",\n            start_date=datetime(2020, 1, 1),\n            end_date=datetime(2020, 12, 31),\n        )\n        self.assertEqual(len(x.get_list_display_columns()), 13)\n\n    def test_time_series_and_cros_tab(self):\n        pass\n\n    def test_attr_as_column(self):\n        report = GeneratorWithAttrAsColumn()\n        columns_data = report.get_list_display_columns()\n        self.assertEqual(len(columns_data), 3)\n        self.assertEqual(columns_data[0][\"verbose_name\"], \"My Verbose Name\")\n\n    def test_improper_group_by(self):\n        def load():\n            ReportGenerator(OrderLine, group_by=\"no_field\", date_field=\"order__date_placed\")\n\n        self.assertRaises(Exception, load)\n\n    def test_missing_report_model(self):\n        def load():\n            ReportGenerator(report_model=None, group_by=\"product\", date_field=\"order__date_placed\")\n\n        self.assertRaises(Exception, load)\n\n    def test_missing_date_field(self):\n        def load():\n            ReportGenerator(report_model=OrderLine, group_by=\"product\", date_field=\"\", time_series_pattern=\"monthly\")\n\n        self.assertRaises(Exception, load)\n\n    def test_wrong_date_field(self):\n        def load():\n            ReportGenerator(report_model=OrderLine, group_by=\"product\", date_field=\"not_here\")\n\n        self.assertRaises(Exception, load)\n\n    def test_unknown_column(self):\n        def load():\n            ReportGenerator(\n                report_model=OrderLine,\n                group_by=\"product\",\n                date_field=\"order__date_placed\",\n                columns=[\"product\", \"not_here\"],\n            )\n\n        self.assertRaises(Exception, load)\n\n    def test_gather_dependencies_for_time_series(self):\n        report = ReportGenerator(\n            report_model=SimpleSales,\n            group_by=\"client\",\n            columns=[\"slug\", \"name\"],\n            time_series_pattern=\"monthly\",\n            date_field=\"doc_date\",\n            time_series_columns=[\"__debit__\", \"__credit__\", \"__balance__\", \"__total__\"],\n        )\n\n        self.assertTrue(report._report_fields_dependencies)\n\n    def test_group_by_traverse(self):\n        report = ReportGenerator(\n            report_model=SimpleSales,\n            group_by=\"product__category\",\n            columns=[\n                \"product__category\",\n                ComputationField.create(Sum, \"value\"),\n                \"__total__\",\n            ],\n            # time_series_pattern='monthly',\n            date_field=\"doc_date\",\n            # time_series_columns=['__debit__', '__credit__', '__balance__', '__total__']\n        )\n\n        self.assertTrue(report._report_fields_dependencies)\n        data = report.get_report_data()\n        self.assertNotEqual(data, [])\n        self.assertEqual(data[0][\"product__category\"], \"small\")\n        self.assertEqual(data[1][\"product__category\"], \"big\")\n\n    def test_group_by_and_foreign_key_field(self):\n        report = ReportGenerator(\n            report_model=SimpleSales,\n            group_by=\"client\",\n            columns=[\n                \"name\",\n                \"contact_id\",\n                \"contact__address\",\n                ComputationField.create(Sum, \"value\"),\n                \"__total__\",\n            ],\n            # time_series_pattern='monthly',\n            date_field=\"doc_date\",\n            # time_series_columns=['__debit__', '__credit__', '__balance__', '__total__']\n        )\n\n        self.assertTrue(report._report_fields_dependencies)\n        data = report.get_report_data()\n        # import pdb;\n        # pdb.set_trace()\n        self.assertNotEqual(data, [])\n        self.assertEqual(data[0][\"name\"], \"Client 1\")\n        self.assertEqual(data[1][\"name\"], \"Client 2\")\n        self.assertEqual(data[2][\"name\"], \"Client 3\")\n\n        self.assertEqual(data[0][\"contact_id\"], 1)\n        self.assertEqual(data[1][\"contact_id\"], 2)\n        self.assertEqual(data[2][\"contact_id\"], 3)\n\n        self.assertEqual(data[0][\"sum__value\"], 300)\n\n        self.assertEqual(Client.objects.get(pk=1).contact.address, \"Street 1\")\n        self.assertEqual(data[0][\"contact__address\"], \"Street 1\")\n        self.assertEqual(data[1][\"contact__address\"], \"Street 2\")\n        self.assertEqual(data[2][\"contact__address\"], \"Street 3\")\n\n    def test_custom_group_by(self):\n        report = ReportGenerator(\n            report_model=SimpleSales,\n            group_by_custom_querysets=[\n                SimpleSales.objects.filter(client_id__in=[self.client1.pk, self.client2.pk]),\n                SimpleSales.objects.filter(client_id__in=[self.client3.pk]),\n            ],\n            group_by_custom_querysets_column_verbose_name=\"Custom Title\",\n            columns=[\n                # \"__index__\", is added automatically\n                ComputationField.create(Sum, \"value\"),\n                \"__total__\",\n            ],\n            date_field=\"doc_date\",\n        )\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2)\n        self.assertEqual(data[0][\"sum__value\"], 900)\n        self.assertEqual(data[1][\"sum__value\"], 1200)\n        self.assertIn(\"__index__\", data[0].keys())\n        columns_data = report.get_columns_data()\n        self.assertEqual(columns_data[0][\"verbose_name\"], \"Custom Title\")\n\n    def test_custom_group_by_with_index(self):\n        report = ReportGenerator(\n            report_model=SimpleSales,\n            group_by_custom_querysets=[\n                SimpleSales.objects.filter(client_id__in=[self.client1.pk, self.client2.pk]),\n                SimpleSales.objects.filter(client_id__in=[self.client3.pk]),\n            ],\n            columns=[\n                \"__index__\",  # assert that no issue if added manually , issue 68\n                ComputationField.create(Sum, \"value\"),\n                \"__total__\",\n            ],\n            date_field=\"doc_date\",\n        )\n\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2)\n        self.assertEqual(data[0][\"sum__value\"], 900)\n        self.assertEqual(data[1][\"sum__value\"], 1200)\n        self.assertIn(\"__index__\", data[0].keys())\n\n    def test_traversing_group_by_and_foreign_key_field(self):\n        report = ReportGenerator(\n            report_model=SimpleSales,\n            group_by=\"client__contact\",\n            columns=[\n                \"po_box\",\n                \"address\",\n                \"agent__name\",\n                ComputationField.create(Sum, \"value\"),\n                \"__total__\",\n            ],\n            date_field=\"doc_date\",\n        )\n\n        self.assertTrue(report._report_fields_dependencies)\n        data = report.get_report_data()\n        self.assertNotEqual(data, [])\n        # self.assertTrue(False)\n        self.assertEqual(data[0][\"address\"], \"Street 1\")\n        self.assertEqual(data[1][\"address\"], \"Street 2\")\n        self.assertEqual(data[1][\"agent__name\"], \"John\")\n        self.assertEqual(data[2][\"agent__name\"], \"Frank\")\n\n    def test_traversing_group_by_sanity(self):\n        report = ReportGenerator(\n            report_model=SimpleSales,\n            group_by=\"client__contact__agent\",\n            columns=[\"name\", ComputationField.create(Sum, \"value\"), \"__total__\"],\n            date_field=\"doc_date\",\n        )\n\n        self.assertTrue(report._report_fields_dependencies)\n        data = report.get_report_data()\n        self.assertNotEqual(data, [])\n        self.assertEqual(len(data), 2)\n\n    def test_db_field_column_verbose_name(self):\n        report = GenericGenerator()\n        field_list = report.get_list_display_columns()\n        self.assertEqual(field_list[0][\"verbose_name\"], \"Client Slug\")\n\n    def test_group_by_char_field(self):\n        report = GroupByCharField()\n        self.assertEqual(len(report.get_list_display_columns()), 3)\n\n\n# test that columns are a straight forward list\nclass TestReportFields(BaseTestData, TestCase):\n    def test_get_full_dependency_list(self):\n        from slick_reporting.fields import BalanceReportField\n\n        deps = BalanceReportField.get_full_dependency_list()\n        self.assertEqual(len(deps), 1)\n\n    def test_computation_field_count(self):\n        # test case for issue #77\n        report = TestCountField()\n        data = report.get_report_data()\n        self.assertEqual(data[0][\"count__id\"], 5)\n        self.assertEqual(data[1][\"count__id\"], 1)\n\n\nclass TestHelpers(TestCase):\n    def test_get_model_for_keys(self):\n        keys = get_foreign_keys(OrderLine)\n        self.assertEqual(len(keys), 3)\n\n\nclass TestListViewGenerator(BaseTestData, TestCase):\n    def test_traversing_field_in_column(self):\n        report = ListViewReportGenerator(\n            report_model=SimpleSales,\n            columns=[\"id\", \"product__name\", \"client__name\", \"value\"],\n            date_field=\"doc_date\",\n        )\n        data = report.get_report_data()\n        self.assertEqual(len(data), SimpleSales.objects.count())\n        self.assertEqual(data[0][\"product__name\"], \"Product 1\")\n        self.assertEqual(data[0][\"client__name\"], \"Client 1\")\n"
  },
  {
    "path": "tests/test_pivot_generator.py",
    "content": "import datetime\n\nfrom django.db import connection\nfrom django.test import TestCase\n\nfrom slick_reporting.dynamic_model import get_dynamic_model, _model_cache\nfrom slick_reporting.generator import ReportGenerator\nfrom tests.models import Agent, Client, Contact, Product, SimpleSales\n\nTABLE_NAME = \"test_pivot_monthly_sales\"\n\nCREATE_TABLE_SQL = f\"\"\"\n    CREATE TABLE {TABLE_NAME} (\n        id INTEGER PRIMARY KEY AUTOINCREMENT,\n        product_id INTEGER NOT NULL,\n        product_name VARCHAR(100) NOT NULL,\n        region VARCHAR(100) NOT NULL,\n        month DATE NOT NULL,\n        total_sales DECIMAL(10, 2) NOT NULL DEFAULT 0,\n        total_quantity INTEGER NOT NULL DEFAULT 0\n    )\n\"\"\"\n\nINSERT_SQL = f\"\"\"\n    INSERT INTO {TABLE_NAME} (product_id, product_name, region, month, total_sales, total_quantity)\n    VALUES (?, ?, ?, ?, ?, ?)\n\"\"\"\n\n\nclass PrecomputedCrosstabTestBase(TestCase):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        with connection.cursor() as cursor:\n            cursor.execute(CREATE_TABLE_SQL)\n            rows = [\n                (1, \"Product A\", \"North\", \"2024-01-01\", 500, 10),\n                (1, \"Product A\", \"North\", \"2024-02-01\", 600, 12),\n                (1, \"Product A\", \"North\", \"2024-03-01\", 550, 11),\n                (2, \"Product B\", \"South\", \"2024-01-01\", 300, 5),\n                (2, \"Product B\", \"South\", \"2024-02-01\", 400, 8),\n                # Product B has no March data — tests missing period\n            ]\n            cursor.executemany(INSERT_SQL, rows)\n\n    @classmethod\n    def tearDownClass(cls):\n        with connection.cursor() as cursor:\n            cursor.execute(f\"DROP TABLE IF EXISTS {TABLE_NAME}\")\n        keys_to_remove = [k for k in _model_cache if k.endswith(f\":{TABLE_NAME}\")]\n        for k in keys_to_remove:\n            del _model_cache[k]\n        from django.apps import apps\n\n        model_key = TABLE_NAME.replace(\"_\", \"\").lower()\n        try:\n            del apps.all_models[\"slick_reporting\"][model_key]\n        except KeyError:\n            pass\n        super().tearDownClass()\n\n\nclass TestPrecomputedCrosstabBasic(PrecomputedCrosstabTestBase):\n    def test_date_crosstab(self):\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            date_field=\"month\",\n            crosstab_field=\"month\",\n            crosstab_columns=[\"total_sales\", \"total_quantity\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2)\n\n        # Find Product A (id=1)\n        prod_a = next(row for row in data if row[\"product_id\"] == 1)\n        # Should have sales for all 3 months\n        self.assertEqual(prod_a[\"total_salesCT2024_01_01\"], 500)\n        self.assertEqual(prod_a[\"total_salesCT2024_02_01\"], 600)\n        self.assertEqual(prod_a[\"total_salesCT2024_03_01\"], 550)\n        self.assertEqual(prod_a[\"total_quantityCT2024_01_01\"], 10)\n\n    def test_missing_period_defaults_to_zero(self):\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            date_field=\"month\",\n            crosstab_field=\"month\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        data = report.get_report_data()\n        prod_b = next(row for row in data if row[\"product_id\"] == 2)\n        # Product B has no March data\n        self.assertEqual(prod_b[\"total_salesCT2024_03_01\"], 0)\n\n    def test_entity_crosstab(self):\n        \"\"\"Crosstab on a non-date field (region).\n        Note: precomputed crosstab reads pre-computed data, it does NOT aggregate.\n        When multiple rows exist for the same (group, crosstab_value),\n        the last row encountered wins.\n        \"\"\"\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            crosstab_field=\"region\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2)\n\n        prod_a = next(row for row in data if row[\"product_id\"] == 1)\n        # Product A has multiple rows in \"North\" — last one wins\n        self.assertIn(\"total_salesCTNorth\", prod_a)\n        self.assertGreater(prod_a[\"total_salesCTNorth\"], 0)\n\n    def test_multiple_crosstab_columns(self):\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            date_field=\"month\",\n            crosstab_field=\"month\",\n            crosstab_columns=[\"total_sales\", \"total_quantity\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        columns_data = report.get_columns_data()\n        col_names = [c[\"name\"] for c in columns_data]\n        # Should have both total_sales and total_quantity for each month\n        self.assertIn(\"total_salesCT2024_01_01\", col_names)\n        self.assertIn(\"total_quantityCT2024_01_01\", col_names)\n        self.assertIn(\"total_salesCT2024_02_01\", col_names)\n        self.assertIn(\"total_quantityCT2024_02_01\", col_names)\n\n\nclass TestPrecomputedCrosstabMetadata(PrecomputedCrosstabTestBase):\n    def test_crosstab_metadata_populated(self):\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            date_field=\"month\",\n            crosstab_field=\"month\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        metadata = report.get_metadata()\n        self.assertEqual(metadata[\"crosstab_model\"], \"month\")\n        self.assertTrue(len(metadata[\"crosstab_column_names\"]) > 0)\n        self.assertTrue(len(metadata[\"crosstab_column_verbose_names\"]) > 0)\n        # Time series should be empty\n        self.assertFalse(metadata[\"time_series_pattern\"])\n        self.assertEqual(metadata[\"time_series_column_names\"], [])\n\n    def test_column_computation_field_attribute(self):\n        \"\"\"Chart JS uses computation_field to match data_source.\"\"\"\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            date_field=\"month\",\n            crosstab_field=\"month\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        columns_data = report.get_columns_data()\n        ct_cols = [c for c in columns_data if \"CT\" in c[\"name\"]]\n        for col in ct_cols:\n            self.assertEqual(col[\"computation_field\"], \"total_sales\")\n\n\nclass TestPrecomputedCrosstabWithTableName(PrecomputedCrosstabTestBase):\n    def test_table_name_convenience(self):\n        report = ReportGenerator(\n            table_name=TABLE_NAME,\n            group_by=\"product_id\",\n            date_field=\"month\",\n            crosstab_field=\"month\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2)\n\n\nclass TestPrecomputedCrosstabDateFiltering(PrecomputedCrosstabTestBase):\n    def test_date_filter_limits_crosstab_values(self):\n        model = get_dynamic_model(TABLE_NAME)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            date_field=\"month\",\n            crosstab_field=\"month\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 2, 1),\n        )\n        report.get_report_data()\n        columns_data = report.get_columns_data()\n        ct_col_names = [c[\"name\"] for c in columns_data if \"CT\" in c[\"name\"]]\n        # Should only have January (end_date filter is __lte so Feb 1 is included)\n        self.assertTrue(all(\"2024_03_01\" not in n for n in ct_col_names))\n\n\nSPACES_TABLE = \"test_pivot_spaces\"\n\nCREATE_SPACES_TABLE_SQL = f\"\"\"\n    CREATE TABLE {SPACES_TABLE} (\n        id INTEGER PRIMARY KEY AUTOINCREMENT,\n        product_id INTEGER NOT NULL,\n        city VARCHAR(100) NOT NULL,\n        total_sales DECIMAL(10, 2) NOT NULL DEFAULT 0\n    )\n\"\"\"\n\nINSERT_SPACES_SQL = f\"\"\"\n    INSERT INTO {SPACES_TABLE} (product_id, city, total_sales) VALUES (?, ?, ?)\n\"\"\"\n\n\nclass TestPrecomputedCrosstabWithSpaces(TestCase):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        with connection.cursor() as cursor:\n            cursor.execute(CREATE_SPACES_TABLE_SQL)\n            rows = [\n                (1, \"New York\", 500),\n                (1, \"Los Angeles\", 300),\n                (2, \"New York\", 200),\n                (2, \"Los Angeles\", 400),\n                (1, \"Q1/2024\", 100),\n            ]\n            cursor.executemany(INSERT_SPACES_SQL, rows)\n\n    @classmethod\n    def tearDownClass(cls):\n        with connection.cursor() as cursor:\n            cursor.execute(f\"DROP TABLE IF EXISTS {SPACES_TABLE}\")\n        keys_to_remove = [k for k in _model_cache if k.endswith(f\":{SPACES_TABLE}\")]\n        for k in keys_to_remove:\n            del _model_cache[k]\n        from django.apps import apps\n\n        model_key = SPACES_TABLE.replace(\"_\", \"\").lower()\n        try:\n            del apps.all_models[\"slick_reporting\"][model_key]\n        except KeyError:\n            pass\n        super().tearDownClass()\n\n    def test_crosstab_values_with_spaces(self):\n        model = get_dynamic_model(SPACES_TABLE)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            crosstab_field=\"city\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n        )\n        data = report.get_report_data()\n        prod_1 = next(row for row in data if row[\"product_id\"] == 1)\n\n        # Spaces sanitized to underscores in column names\n        self.assertEqual(prod_1[\"total_salesCTNew_York\"], 500)\n        self.assertEqual(prod_1[\"total_salesCTLos_Angeles\"], 300)\n\n    def test_crosstab_values_with_special_chars(self):\n        model = get_dynamic_model(SPACES_TABLE)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            crosstab_field=\"city\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n        )\n        data = report.get_report_data()\n        prod_1 = next(row for row in data if row[\"product_id\"] == 1)\n\n        # Slash sanitized to underscore\n        self.assertEqual(prod_1[\"total_salesCTQ1_2024\"], 100)\n\n    def test_verbose_name_preserves_original(self):\n        model = get_dynamic_model(SPACES_TABLE)\n        report = ReportGenerator(\n            report_model=model,\n            group_by=\"product_id\",\n            crosstab_field=\"city\",\n            crosstab_columns=[\"total_sales\"],\n            crosstab_precomputed=True,\n            columns=[\"product_id\", \"__crosstab__\"],\n        )\n        columns_data = report.get_columns_data()\n        ny_col = next(c for c in columns_data if \"New_York\" in c[\"name\"])\n        self.assertIn(\"New York\", ny_col[\"verbose_name\"])\n\n\nclass TestPrecomputedCrosstabWithFKGroupBy(TestCase):\n    \"\"\"Regression: precomputed crosstab with a ForeignKey group_by returned empty rows.\n\n    prepare_queryset returned .values(\"product_id\") so each obj was {\"product_id\": N}.\n    _get_record_data then looked up obj[\"id\"] (the related model PK) which was missing,\n    causing group_by_val=\"None\" and every precomputed lookup to return 0.\n\n    Fix: mirror the non-precomputed FK path and fetch related-model objects so obj[\"id\"]\n    and obj[\"name\"] are available.\n    \"\"\"\n\n    @classmethod\n    def setUpTestData(cls):\n        agent = Agent.objects.create(name=\"Agent FK\")\n        contact = Contact.objects.create(address=\"Addr\", agent=agent)\n        cls.product1 = Product.objects.create(name=\"Prod FK 1\", category=\"small\", sku=\"fk1\", notes=\"\", slug=\"fk1\")\n        cls.product2 = Product.objects.create(name=\"Prod FK 2\", category=\"medium\", sku=\"fk2\", notes=\"\", slug=\"fk2\")\n        cls.client1 = Client.objects.create(name=\"Cli FK 1\", notes=\"\", slug=\"cfk1\")\n        cls.client1.contact = contact\n        cls.client1.save()\n        cls.client2 = Client.objects.create(name=\"Cli FK 2\", notes=\"\", slug=\"cfk2\")\n        cls.client2.contact = contact\n        cls.client2.save()\n        SimpleSales.objects.create(\n            slug=\"s1\", doc_date=datetime.datetime(2024, 1, 15), client=cls.client1,\n            product=cls.product1, quantity=5, price=100, created_at=datetime.datetime(2024, 1, 15),\n        )\n        SimpleSales.objects.create(\n            slug=\"s2\", doc_date=datetime.datetime(2024, 2, 15), client=cls.client2,\n            product=cls.product2, quantity=3, price=200, created_at=datetime.datetime(2024, 2, 15),\n        )\n\n    def test_rows_populated_with_fk_group_by(self):\n        report = ReportGenerator(\n            report_model=SimpleSales,\n            group_by=\"product\",\n            date_field=\"doc_date\",\n            crosstab_field=\"client\",\n            crosstab_columns=[\"value\"],\n            crosstab_precomputed=True,\n            columns=[\"name\", \"__crosstab__\"],\n            start_date=datetime.datetime(2024, 1, 1),\n            end_date=datetime.datetime(2024, 12, 31),\n        )\n        data = report.get_report_data()\n\n        self.assertEqual(len(data), 2, \"Expected one row per product\")\n\n        names = {row[\"name\"] for row in data}\n        self.assertEqual(names, {\"Prod FK 1\", \"Prod FK 2\"}, \"Product names must be populated (not empty strings)\")\n\n        prod1_row = next(row for row in data if row[\"name\"] == \"Prod FK 1\")\n        client1_col = f\"valueCT{self.client1.pk}\"\n        self.assertEqual(\n            prod1_row[client1_col], 500,\n            \"Prod FK 1 / Client 1 value should be 500 (5 * 100); was 0 before the fix\",\n        )\n"
  },
  {
    "path": "tests/tests.py",
    "content": "import datetime\nfrom unittest import skip\nfrom unittest.mock import patch\n\nfrom django.contrib.auth import get_user_model\nfrom django.db.models import Count\nfrom django.test import TestCase, override_settings\nfrom django.urls import reverse\nfrom django.utils.timezone import now\n\nfrom slick_reporting.fields import ComputationField, BalanceReportField\nfrom slick_reporting.generator import ReportGenerator\nfrom slick_reporting.views import ReportView\nfrom slick_reporting.registry import field_registry\nfrom tests.report_generators import (\n    ClientTotalBalance,\n    ProductClientSalesMatrix2,\n    GroupByCharField,\n    GroupByCharFieldPlusTimeSeries,\n    TimeSeriesWithOutGroupBy,\n    ProductClientSalesMatrixwSimpleSales2,\n)\nfrom . import report_generators\nfrom .models import (\n    Client,\n    Contact,\n    Product,\n    SimpleSales,\n    UserJoined,\n    SalesWithFlag,\n    ComplexSales,\n    TaxCode,\n    ProductCustomID,\n    SalesProductWithCustomID,\n    Agent,\n    SimpleSales2,\n)\n\nUser = get_user_model()\nSUPER_LOGIN = dict(username=\"superlogin\", password=\"password\")\nyear = now().year\n\n\nclass BaseTestData:\n    databases = \"__all__\"\n\n    @classmethod\n    def setUpTestData(cls):\n        super().setUpTestData()\n\n        User.objects.create_superuser(\"super\", None, \"secret\")\n\n        user = User.objects.create(is_superuser=True, is_staff=True, **SUPER_LOGIN)\n        limited_user = User.objects.create_user(\n            is_superuser=False, is_staff=True, username=\"limited\", password=\"password\"\n        )\n        cls.user = user\n        cls.limited_user = limited_user\n        agent = Agent.objects.create(name=\"John\")\n        agent2 = Agent.objects.create(name=\"Frank\")\n        cls.client1 = Client.objects.create(name=\"Client 1\", sex=\"MALE\")\n        cls.client1.contact = Contact.objects.create(address=\"Street 1\", agent=agent)\n        cls.client1.save()\n        cls.client2 = Client.objects.create(name=\"Client 2\", sex=\"FEMALE\")\n        cls.client2.contact = Contact.objects.create(address=\"Street 2\", agent=agent)\n        cls.client2.save()\n        cls.client3 = Client.objects.create(name=\"Client 3\", sex=\"OTHER\")\n        cls.client3.contact = Contact.objects.create(address=\"Street 3\", agent=agent2)\n        cls.client3.save()\n        cls.clientIdle = Client.objects.create(name=\"Client Idle\")\n\n        cls.product1 = Product.objects.create(name=\"Product 1\", category=\"small\", sku=\"a1b1\")\n        cls.product2 = Product.objects.create(name=\"Product 2\", category=\"medium\", sku=\"a2b2\")\n        cls.product3 = Product.objects.create(name=\"Product 3\", category=\"big\", sku=\"3333\")\n\n        cls.product_w_custom_id1 = ProductCustomID.objects.create(name=\"Product 1\", category=\"small\")\n        cls.product_w_custom_id2 = ProductCustomID.objects.create(name=\"Product 2\", category=\"medium\")\n\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 1, 2),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 1, 5),\n        )\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 2, 2),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 2, 3),\n        )\n\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 3, 3),\n        )\n\n        # client 2\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 1, 2),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 2, 2),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n\n        # client 3\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 1, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 2, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n\n        SimpleSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 1, 2),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 1, 5),\n        )\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 2, 2),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 2, 3),\n        )\n\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 3, 3),\n        )\n\n        # client 2\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 1, 2),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 2, 2),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n\n        # client 3\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 1, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 2, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n\n        SimpleSales2.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n\n        cls.tax1 = TaxCode.objects.create(name=\"State\", tax=8)  # Added three times\n        cls.tax2 = TaxCode.objects.create(name=\"Vat reduced\", tax=5)  # Added two times\n        cls.tax3 = TaxCode.objects.create(name=\"Vat full\", tax=20)  # Added one time\n\n        sale1 = ComplexSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n            flag=\"sales\",\n        )\n        sale2 = ComplexSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n            flag=\"sales\",\n        )\n        sale3 = ComplexSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n            flag=\"sales\",\n        )\n        sale4 = ComplexSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n            flag=\"sales-return\",\n        )\n        sale4 = ComplexSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client3,\n            product=cls.product2,\n            quantity=34,\n            price=10,\n            flag=\"sales-return\",\n        )\n        ComplexSales.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=77,\n            price=10,\n            flag=\"\",\n        )\n        sale1.tax.add(cls.tax1)\n        sale1.tax.add(cls.tax2)\n        sale2.tax.add(cls.tax1)\n        sale2.tax.add(cls.tax3)\n        sale3.tax.add(cls.tax1)\n        sale4.tax.add(cls.tax2)\n\n        SalesProductWithCustomID.objects.create(\n            doc_date=datetime.datetime(year, 1, 2),\n            client=cls.client1,\n            product=cls.product_w_custom_id1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 1, 5),\n        )\n        SalesProductWithCustomID.objects.create(\n            doc_date=datetime.datetime(year, 2, 2),\n            client=cls.client1,\n            product=cls.product_w_custom_id1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 2, 3),\n        )\n\n        SalesProductWithCustomID.objects.create(\n            doc_date=datetime.datetime(year, 3, 2),\n            client=cls.client1,\n            product=cls.product_w_custom_id2,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 3, 3),\n        )\n\n\n# @override_settings(ROOT_URLCONF='reporting_tests.urls', RA_CACHE_REPORTS=False, USE_TZ=False)\nclass ReportTest(BaseTestData, TestCase):\n    def test_client_balance(self):\n        report = report_generators.ClientTotalBalance()\n        data = report.get_report_data()\n\n        self.assertEqual(data[0].get(\"__balance__\"), 300, data[0])\n\n    def test_compute_from_queryset(self):\n        report = report_generators.TotalBalanceWithQueryset()\n        data = report.get_report_data()\n        self.assertEqual(data, [])\n\n    def test_product_total_sales(self):\n        report = report_generators.ProductTotalSalesProductWithCustomID()\n        data = report.get_report_data()\n        self.assertEqual(data[0][\"__balance__\"], 200)\n        self.assertEqual(data[1][\"__balance__\"], 100)\n\n    def test_product_total_sales_product_custom_id(self):\n        report = report_generators.ProductTotalSales()\n        data = report.get_report_data()\n        self.assertEqual(data[0][\"__balance__\"], 1800)\n        self.assertEqual(data[0][\"get_object_sku\"], \"A1B1\")\n        self.assertEqual(\n            data[0][\"average_value\"],\n            data[0][\"__balance__\"] / data[0][\"__balance_quantity__\"],\n        )\n\n    def test_product_total_sales_with_percentage(self):\n        report = report_generators.ProductTotalSalesWithPercentage()\n        data = report.get_report_data()\n        self.assertEqual(data[2][\"__percent_to_total_balance__\"], 50)\n\n    @override_settings(\n        SLICK_REPORTING_DEFAULT_START_DATE=datetime.datetime(2020, 1, 1),\n        SLICK_REPORTING_DEFAULT_END_DATE=datetime.datetime(2021, 1, 1),\n    )\n    def test_product_total_sales_with_changed_dated(self):\n        report = report_generators.ProductTotalSales()\n        data = report.get_report_data()\n        self.assertEqual(len(data), 0)\n\n    def test_client_client_sales_monthly(self):\n        report = report_generators.ClientSalesMonthlySeries()\n\n        data = report.get_report_data()\n\n        self.assertEqual(data[0].get(\"__balance__TS%s0301\" % year), 200, data[0])\n        self.assertEqual(data[0][\"__balance__TS%s0201\" % year], 100)\n\n        self.assertEqual(data[0][\"__total__TS%s0401\" % year], 100)\n        self.assertEqual(data[0][\"__total__TS%s0301\" % year], 100)\n        self.assertEqual(data[0][\"__total__TS%s0201\" % year], 100)\n\n        self.assertEqual(data[0][\"__debit__TS%s0401\" % year], 100)\n        self.assertEqual(data[0][\"__debit__TS%s0301\" % year], 100)\n        self.assertEqual(data[0][\"__debit__TS%s0201\" % year], 100)\n\n        self.assertEqual(data[2][\"__debit__TS%s0401\" % year], 300)\n        self.assertEqual(data[2][\"__debit__TS%s0301\" % year], 300)\n        self.assertEqual(data[2][\"__debit__TS%s0201\" % year], 300)\n\n        # todo add __fb__ to time series and check the balance\n\n    def test_productclientsalesmatrix(self):\n        report = report_generators.ProductClientSalesMatrix(crosstab_ids=[self.client1.pk, self.client2.pk])\n        data = report.get_report_data()\n        self.assertEqual(data[0][\"__total__CT%s\" % self.client1.pk], 300)\n        self.assertEqual(data[0][\"__total__CT%s\" % self.client2.pk], 600)\n        self.assertEqual(data[0][\"__total__CT----\"], 900)\n\n    def test_productclientsalesmatrix_no_remainder(self):\n        report = report_generators.ProductClientSalesMatrix(\n            crosstab_ids=[self.client1.pk, self.client2.pk],\n            crosstab_compute_remainder=False,\n        )\n        data = report.get_report_data()\n        self.assertEqual(data[0][\"__total__CT%s\" % self.client1.pk], 300)\n        self.assertEqual(data[0][\"__total__CT%s\" % self.client2.pk], 600)\n\n    def test_show_empty_records(self):\n        report = report_generators.ClientTotalBalance()\n        data = report.get_report_data()\n        with_show_empty_len = len(data)\n        wo_show_empty = report_generators.ClientTotalBalance(show_empty_records=False)\n        self.assertNotEqual(with_show_empty_len, wo_show_empty)\n        # self.assertEqual(data[0].get('__balance__'), 300, data[0])\n\n    def test_filters(self):\n        report = ClientTotalBalance(kwargs_filters={\"client\": self.client1.pk}, show_empty_records=True)\n        data = report.get_report_data()\n        self.assertEqual(len(data), 1, data)\n\n        report = ClientTotalBalance(kwargs_filters={\"client\": self.client1.pk}, show_empty_records=False)\n        data = report.get_report_data()\n        self.assertEqual(len(data), 1, data)\n\n    def test_view_filter_to_field_set(self):\n        report_generator = ReportGenerator(\n            report_model=SimpleSales2,\n            date_field=\"doc_date\",\n            group_by=\"client\",\n            columns=[\"slug\", \"name\"],\n            time_series_pattern=\"monthly\",\n            time_series_columns=[\"__total__\", \"__balance__\"],\n        )\n        data = report_generator.get_report_data()\n\n        with patch(\"slick_reporting.helpers.user_test_function\", return_value=True):\n            response = self.client.get(\n                reverse(\"report-to-field-set\"),\n                data={\n                    \"client_id\": [self.client2.name, self.client1.name],\n                },\n                HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n            )\n        self.assertEqual(response.status_code, 200)\n\n        response.json()\n        self.assertTrue(len(data), 2)\n        # self.assertEqual(view_report_data['data'], data)\n\n    def test_filter_as_int_n_list(self):\n        report = ClientTotalBalance(kwargs_filters={\"client\": self.client1.pk}, show_empty_records=True)\n        data = report.get_report_data()\n        self.assertEqual(len(data), 1, data)\n\n        report = ClientTotalBalance(kwargs_filters={\"client_id__in\": [self.client1.pk]}, show_empty_records=True)\n        data = report.get_report_data()\n        self.assertEqual(len(data), 1, data)\n\n    def test_timeseries_without_group(self):\n        report = TimeSeriesWithOutGroupBy()\n        data = report.get_report_data()\n        self.assertEqual(data[0][f\"__total__TS{year}0201\"], 600)\n\n    def test_many_to_many_group_by(self):\n        field_registry.register(ComputationField.create(Count, \"tax__name\", \"tax__count\"))\n\n        report_generator = ReportGenerator(\n            report_model=ComplexSales,\n            date_field=\"doc_date\",\n            group_by=\"tax__name\",\n            columns=[\"tax__name\", \"tax__count\"],\n        )\n        data = report_generator.get_report_data()\n\n        self.assertEqual(len(data), 4)  # 3 taxes + 1 empty\n        self.assertEqual(data[0][\"tax__name\"], \"State\")\n        self.assertEqual(data[0][\"tax__count\"], 3)\n        self.assertEqual(data[1][\"tax__name\"], \"Vat reduced\")\n        self.assertEqual(data[1][\"tax__count\"], 2)\n        self.assertEqual(data[2][\"tax__name\"], \"Vat full\")\n        self.assertEqual(data[2][\"tax__count\"], 1)\n\n\nclass TestView(BaseTestData, TestCase):\n    def test_view(self):\n        response = self.client.get(\n            reverse(\"report1\"),\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        view_report_data = response.json()[\"data\"]\n        report_generator = ReportGenerator(\n            report_model=SimpleSales,\n            date_field=\"doc_date\",\n            group_by=\"client\",\n            columns=[\"slug\", \"name\"],\n            time_series_pattern=\"monthly\",\n            time_series_columns=[\"__total__\", \"__balance__\"],\n        )\n        self.assertTrue(view_report_data)\n        self.assertEqual(view_report_data, report_generator.get_report_data())\n\n    def test_qs_only(self):\n        response = self.client.get(\n            reverse(\"queryset-only\"),\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        view_report_data = response.json()[\"data\"]\n        report_generator = ReportGenerator(\n            report_model=SimpleSales,\n            date_field=\"doc_date\",\n            group_by=\"client\",\n            columns=[\"slug\", \"name\"],\n            time_series_pattern=\"monthly\",\n            time_series_columns=[\"__total__\", \"__balance__\"],\n        )\n        self.assertTrue(view_report_data)\n        self.assertEqual(view_report_data, report_generator.get_report_data())\n\n    def test_view_filter(self):\n        report_generator = ReportGenerator(\n            report_model=SimpleSales,\n            date_field=\"doc_date\",\n            group_by=\"client\",\n            columns=[\"slug\", \"name\"],\n            time_series_pattern=\"monthly\",\n            time_series_columns=[\"__total__\", \"__balance__\"],\n            kwargs_filters={\"client_id__in\": [self.client1.pk, self.client2.pk]},\n        )\n        data = report_generator.get_report_data()\n        response = self.client.get(\n            reverse(\"report1\"),\n            data={\n                \"client_id\": [self.client2.pk, self.client1.pk],\n            },\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        self.assertTrue(len(data), 2)\n        view_report_data = response.json()\n        self.assertEqual(view_report_data[\"data\"], data)\n\n    def test_view_filter_to_field_set(self):\n        report_generator = ReportGenerator(\n            report_model=SimpleSales2,\n            date_field=\"doc_date\",\n            group_by=\"client\",\n            columns=[\"slug\", \"name\"],\n            time_series_pattern=\"monthly\",\n            time_series_columns=[\"__total__\", \"__balance__\"],\n        )\n        data = report_generator.get_report_data()\n        response = self.client.get(\n            reverse(\"report-to-field-set\"),\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n\n        self.assertTrue(len(data), 2)\n\n        view_report_data = response.json()\n\n        self.assertEqual(view_report_data[\"data\"], data)\n\n    def test_ajax(self):\n        report_generator = ReportGenerator(\n            report_model=SimpleSales,\n            date_field=\"doc_date\",\n            group_by=\"client\",\n            columns=[\"slug\", \"name\"],\n            time_series_pattern=\"monthly\",\n            time_series_columns=[\"__total__\", \"__balance__\"],\n        )\n        data = report_generator.get_report_data()\n        response = self.client.get(reverse(\"report1\"), HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\")\n        self.assertEqual(response.status_code, 200)\n        view_report_data = response.json()\n        self.assertEqual(view_report_data[\"data\"], data)\n\n    def test_crosstab_report_view(self):\n        from .report_generators import ProductClientSalesMatrix\n\n        data = ProductClientSalesMatrix(\n            crosstab_compute_remainder=True,\n            crosstab_ids=[self.client1.pk, self.client2.pk],\n        ).get_report_data()\n\n        response = self.client.get(reverse(\"product_crosstab_client\"))\n        self.assertEqual(response.status_code, 200)\n        response = self.client.get(\n            reverse(\"product_crosstab_client\"),\n            data={\n                \"client_id\": [self.client1.pk, self.client2.pk],\n                \"crosstab_compute_remainder\": True,\n            },\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        view_report_data = response.json()\n        self.assertEqual(view_report_data[\"data\"], data)\n\n    def test_crosstab_report_view_clumns_on_fly(self):\n        data = ProductClientSalesMatrix2(\n            crosstab_compute_remainder=True,\n            crosstab_ids=[self.client1.pk, self.client2.pk],\n        ).get_report_data()\n\n        response = self.client.get(\n            reverse(\"crosstab-columns-on-fly\"),\n            data={\n                \"client_id\": [self.client1.pk, self.client2.pk],\n                \"crosstab_compute_remainder\": True,\n            },\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        view_report_data = response.json()\n        self.assertEqual(view_report_data[\"data\"], data, view_report_data)\n\n    def test_crosstab_report_view_to_field_set(self):\n        from .report_generators import ProductClientSalesMatrixToFieldSet\n\n        data = ProductClientSalesMatrixToFieldSet(\n            crosstab_compute_remainder=True,\n            crosstab_ids=[self.client1.name, self.client2.name],\n        ).get_report_data()\n\n        response = self.client.get(reverse(\"product_crosstab_client_to_field_set\"))\n        self.assertEqual(response.status_code, 200)\n        response = self.client.get(\n            reverse(\"product_crosstab_client_to_field_set\"),\n            data={\n                \"client_id\": [self.client1.name, self.client2.name],\n                \"crosstab_compute_remainder\": True,\n            },\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        view_report_data = response.json()\n        self.assertEqual(view_report_data[\"data\"], data)\n\n    def test_crosstab_report_view_clumns_on_fly_to_field_set(self):\n        data = ProductClientSalesMatrixwSimpleSales2(\n            crosstab_compute_remainder=True,\n            crosstab_ids=[self.client1.name, self.client2.name],\n        ).get_report_data()\n\n        response = self.client.get(\n            reverse(\"crosstab-columns-on-fly-to-field-set\"),\n            data={\n                \"client_id\": [self.client1.name, self.client2.name],\n                \"crosstab_compute_remainder\": True,\n            },\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        view_report_data = response.json()\n        self.assertEqual(view_report_data[\"data\"], data, view_report_data[\"data\"])\n\n    def test_chart_settings(self):\n        response = self.client.get(\n            reverse(\"product_crosstab_client\"),\n            data={\n                \"client_id\": [self.client1.pk, self.client2.pk],\n                \"crosstab_compute_remainder\": True,\n            },\n            HTTP_X_REQUESTED_WITH=\"XMLHttpRequest\",\n        )\n        self.assertEqual(response.status_code, 200)\n        data = response.json()\n        self.assertTrue(data[\"chart_settings\"][0][\"id\"] != \"\")\n        self.assertTrue(data[\"chart_settings\"][0][\"title\"], \"awesome report title\")\n\n    @skip\n    def test_error_on_missing_date_field(self):\n        def test_function():\n            class TotalClientSales(ReportView):\n                report_model = SimpleSales\n\n        self.assertRaises(TypeError, test_function)\n\n\nclass TestReportFieldRegistry(TestCase):\n    def test_unregister(self):\n        # unregister a field that we know exists\n        field_registry.unregister(\"__balance__\")\n        self.assertNotIn(\"__balance__\", field_registry.get_all_report_fields_names())\n        # bring it back again as later tests using it would fail\n        field_registry.register(BalanceReportField)\n\n    def test_registering_new(self):\n        def register():\n            class ReportFieldWDuplicatedName(ComputationField):\n                name = \"__total_field__\"\n                calculation_field = \"field\"\n\n            field_registry.register(ReportFieldWDuplicatedName)\n\n        register()\n        self.assertIn(\"__total_field__\", field_registry.get_all_report_fields_names())\n\n    def test_already_registered(self):\n        def register():\n            class ReportFieldWDuplicatedName(ComputationField):\n                name = \"__total__\"\n\n            field_registry.register(ReportFieldWDuplicatedName)\n\n        with self.assertRaises(Exception):\n            register()\n\n    def test_unregister_a_non_existent(self):\n        def register():\n            field_registry.unregister(\"__a_weird_name__\")\n\n        with self.assertRaises(Exception):\n            register()\n\n    def test_get_non_existent_field(self):\n        def register():\n            return field_registry.get_field_by_name(\"__a_weird_name__\")\n\n        with self.assertRaises(Exception):\n            register()\n\n    def test_creating_a_report_field_on_the_fly(self):\n        from django.db.models import Sum\n\n        name = ComputationField.create(Sum, \"value\", \"__sum_of_value__\")\n        self.assertNotIn(name, field_registry.get_all_report_fields_names())\n\n    def test_creating_a_report_field_on_the_fly_wo_name(self):\n        from django.db.models import Sum\n\n        name = ComputationField.create(Sum, \"value\")\n        self.assertNotIn(name, field_registry.get_all_report_fields_names())\n\n\nclass TestGroupByDate(TestCase):\n    @classmethod\n    def setUpTestData(cls):\n        super().setUpTestData()\n        UserJoined.objects.create(username=\"adam\", date_joined=datetime.date(2020, 1, 2))\n        UserJoined.objects.create(username=\"eve\", date_joined=datetime.date(2020, 1, 3))\n        UserJoined.objects.create(username=\"steve\", date_joined=datetime.date(2020, 1, 5))\n        UserJoined.objects.create(username=\"smiv\", date_joined=datetime.date(2020, 1, 5))\n\n    def test_joined_per_day(self):\n        field_registry.register(ComputationField.create(Count, \"id\", \"count__id\"))\n        report_generator = ReportGenerator(\n            report_model=UserJoined,\n            date_field=\"date_joined\",\n            group_by=\"date_joined\",\n            start_date=datetime.date(2020, 1, 1),\n            end_date=datetime.date(2020, 1, 10),\n            columns=[\"date_joined\", \"count__id\"],\n        )\n\n        data = report_generator.get_report_data()\n        self.assertEqual(len(data), 3)\n        self.assertEqual(data[0][\"count__id\"], 1)\n        self.assertEqual(data[1][\"count__id\"], 1)\n        self.assertEqual(data[2][\"count__id\"], 2)\n\n\nclass TestGroupByFlag(TestCase):\n    databases = \"__all__\"\n\n    @classmethod\n    def setUpTestData(cls):\n        super().setUpTestData()\n\n        User.objects.create_superuser(\"super\", None, \"secret\")\n\n        user = User.objects.create(is_superuser=True, is_staff=True, **SUPER_LOGIN)\n        limited_user = User.objects.create_user(\n            is_superuser=False, is_staff=True, username=\"limited\", password=\"password\"\n        )\n        cls.user = user\n        cls.limited_user = limited_user\n        cls.client1 = Client.objects.create(name=\"Client 1\")\n        cls.client2 = Client.objects.create(name=\"Client 2\")\n        cls.client3 = Client.objects.create(name=\"Client 3\")\n        cls.clientIdle = Client.objects.create(name=\"Client Idle\")\n\n        cls.product1 = Product.objects.create(name=\"Product 1\")\n        cls.product2 = Product.objects.create(name=\"Product 2\")\n        cls.product3 = Product.objects.create(name=\"Product 3\")\n\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 1, 1),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 1, 5),\n        )\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 2, 1),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 2, 3),\n        )\n\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 3, 1),\n            client=cls.client1,\n            product=cls.product1,\n            quantity=10,\n            price=10,\n            created_at=datetime.datetime(year, 3, 3),\n        )\n\n        # client 2\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 1, 1),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 2, 1),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 3, 1),\n            client=cls.client2,\n            product=cls.product1,\n            quantity=20,\n            price=10,\n        )\n\n        # client 3\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 1, 1),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 2, 1),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 3, 1),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=30,\n            price=10,\n        )\n        SalesWithFlag.objects.create(\n            doc_date=datetime.datetime(year, 3, 1),\n            client=cls.client3,\n            product=cls.product1,\n            quantity=25,\n            price=10,\n            flag=\"sales-return\",\n        )\n\n    def test_group_by_flag(self):\n        report = GroupByCharField()\n        data = report.get_report_data()\n        self.assertEqual(data[0][\"sum__quantity\"], 180)\n        self.assertEqual(data[1][\"sum__quantity\"], 25)\n\n    def test_group_by_flag_time_series(self):\n        report = GroupByCharFieldPlusTimeSeries()\n        data = report.get_report_data()\n        self.assertEqual(len(data), 2)\n        self.assertEqual(data[1][\"sum__quantity\"], 25)\n        self.assertEqual(data[1][f\"sum__quantityTS{year}0401\"], 25)\n"
  },
  {
    "path": "tests/urls.py",
    "content": "from django.urls import path\nfrom . import views\n\nurlpatterns = [\n    path(\"report1/\", views.MonthlyProductSales.as_view(), name=\"report1\"),\n    path(\n        \"product_crosstab_client/\",\n        views.ProductClientSalesMatrix.as_view(),\n        name=\"product_crosstab_client\",\n    ),\n    path(\n        \"report-to-field-set/\",\n        views.MonthlyProductSalesToFIeldSet.as_view(),\n        name=\"report-to-field-set\",\n    ),\n    path(\n        \"product_crosstab_client/\",\n        views.ProductClientSalesMatrix.as_view(),\n        name=\"product_crosstab_client\",\n    ),\n    path(\n        \"product_crosstab_client-to_field-set/\",\n        views.ProductClientSalesMatrixToFieldSet.as_view(),\n        name=\"product_crosstab_client_to_field_set\",\n    ),\n    path(\n        \"crosstab-columns-on-fly/\",\n        views.CrossTabColumnOnFly.as_view(),\n        name=\"crosstab-columns-on-fly\",\n    ),\n    path(\n        \"crosstab-columns-on-fly-to-field-set/\",\n        views.CrossTabColumnOnFlyToFieldSet.as_view(),\n        name=\"crosstab-columns-on-fly-to-field-set\",\n    ),\n    path(\n        \"queryset-only/\", views.MonthlyProductSalesWQS.as_view(), name=\"queryset-only\"\n    ),\n]\n"
  },
  {
    "path": "tests/views.py",
    "content": "from slick_reporting.views import ReportView\nfrom slick_reporting.fields import ComputationField, TotalReportField\nfrom django.db.models import Sum, Count\nfrom .models import SimpleSales, ComplexSales, SimpleSales2\nfrom django.utils.translation import gettext_lazy as _\n\n\nclass MonthlyProductSales(ReportView):\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n    group_by = \"client\"\n    columns = [\"slug\", \"name\"]\n    time_series_pattern = \"monthly\"\n    time_series_columns = [\"__total__\", \"__balance__\"]\n\n\nclass MonthlyProductSalesToFIeldSet(ReportView):\n    report_model = SimpleSales2\n    date_field = \"doc_date\"\n    group_by = \"client\"\n    columns = [\"slug\", \"name\"]\n    time_series_pattern = \"monthly\"\n    time_series_columns = [\"__total__\", \"__balance__\"]\n\n\nclass ProductClientSalesMatrix(ReportView):\n    report_title = \"awesome report title\"\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n\n    crosstab_field = \"client\"\n    crosstab_columns = [TotalReportField]\n\n    chart_settings = [\n        {\n            \"type\": \"pie\",\n            \"date_source\": \"__total__\",\n            \"title_source\": \"__total__\",\n        }\n    ]\n\n\nclass ProductClientSalesMatrixToFieldSet(ReportView):\n    report_title = \"awesome report title\"\n    report_model = SimpleSales2\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n\n    crosstab_field = \"client\"\n    crosstab_columns = [\"__total__\"]\n\n    chart_settings = [\n        {\n            \"type\": \"pie\",\n            \"date_source\": \"__total__\",\n            \"title_source\": \"__total__\",\n        }\n    ]\n\n\nclass CrossTabColumnOnFly(ReportView):\n    report_title = \"awesome report title\"\n    report_model = SimpleSales\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n\n    crosstab_field = \"client\"\n    crosstab_columns = [\n        ComputationField.create(\n            Sum, \"value\", name=\"value__sum\", verbose_name=_(\"Sales\")\n        )\n    ]\n\n    chart_settings = [\n        {\n            \"type\": \"pie\",\n            \"date_source\": \"value__sum\",\n            \"title_source\": \"name\",\n        }\n    ]\n\n\nclass CrossTabColumnOnFlyToFieldSet(ReportView):\n    report_title = \"awesome report title\"\n    report_model = SimpleSales2\n    date_field = \"doc_date\"\n\n    group_by = \"product\"\n    columns = [\"slug\", \"name\"]\n\n    crosstab_field = \"client\"\n    crosstab_columns = [\n        ComputationField.create(\n            Sum, \"value\", name=\"value__sum\", verbose_name=_(\"Sales\")\n        )\n    ]\n\n    chart_settings = [\n        {\n            \"type\": \"pie\",\n            \"date_source\": \"value__sum\",\n            \"title_source\": \"name\",\n        }\n    ]\n\n\nclass MonthlyProductSalesWQS(ReportView):\n    queryset = SimpleSales.objects.all()\n    date_field = \"doc_date\"\n    group_by = \"client\"\n    columns = [\"slug\", \"name\"]\n    time_series_pattern = \"monthly\"\n    time_series_columns = [TotalReportField, \"__balance__\"]\n\n\nclass TaxSales(ReportView):\n    # report_model = SimpleSales\n    queryset = ComplexSales.objects.all()\n    date_field = \"doc_date\"\n    group_by = \"tax__name\"\n    columns = [\n        \"tax__name\",\n        ComputationField.create(\n            Count, \"tax\", name=\"tax__count\", verbose_name=_(\"Sales\")\n        ),\n    ]\n    chart_settings = [\n        {\n            \"type\": \"pie\",\n            \"date_source\": \"tax__count\",\n            \"title_source\": \"tax__name\",\n        }\n    ]\n\n\nclass MonthlyProductSalesToFIeldSet(ReportView):\n    report_model = SimpleSales2\n    date_field = \"doc_date\"\n    group_by = \"client\"\n    columns = [\"slug\", \"name\"]\n    time_series_pattern = \"monthly\"\n    time_series_columns = [\"__total__\", \"__balance__\"]\n\n\nclass TaxSales(ReportView):\n    # report_model = SimpleSales\n    queryset = ComplexSales.objects.all()\n    date_field = \"doc_date\"\n    group_by = \"tax__name\"\n    columns = [\n        \"tax__name\",\n        ComputationField.create(\n            Count, \"tax\", name=\"tax__count\", verbose_name=_(\"Sales\")\n        ),\n    ]\n    chart_settings = [\n        {\n            \"type\": \"pie\",\n            \"date_source\": \"tax__count\",\n            \"title_source\": \"tax__name\",\n        }\n    ]\n"
  }
]