├── __init__.py ├── example ├── __init__.py ├── orders │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_orderitem_total.py │ │ ├── 0003_auto_20141225_2344.py │ │ ├── 0004_orderitemproxy.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── views.py │ ├── models.py │ └── admin.py ├── products │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_product_value.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── views.py │ ├── models.py │ └── admin.py ├── test_django_admin_report │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── manage.py └── templates │ └── admin │ └── import_export │ └── change_list_export.html ├── admin_report ├── __init__.py ├── static │ └── admin │ │ └── css │ │ └── django_admin_report │ │ └── django_admin_report.css ├── templates │ └── admin │ │ └── change_list_results.html └── mixins.py ├── setup.cfg ├── MANIFEST.in ├── .gitignore ├── LICENSE.txt ├── setup.py └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/orders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/products/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/orders/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/products/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin_report/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.6.2' 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include admin_report/templates * 3 | -------------------------------------------------------------------------------- /example/orders/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example/orders/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /example/products/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example/products/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. -------------------------------------------------------------------------------- /admin_report/static/admin/css/django_admin_report/django_admin_report.css: -------------------------------------------------------------------------------- 1 | #result_list tfoot td{ 2 | padding: 5px 10px 4px; 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | local_settings.py 5 | docs/_build 6 | build/ 7 | dist/ 8 | *.egg-info/ 9 | .tox/ 10 | .idea/ -------------------------------------------------------------------------------- /example/test_django_admin_report/__init__.py: -------------------------------------------------------------------------------- 1 | from os import sys, path 2 | sys.path.append(path.dirname(path.dirname(path.dirname(path.abspath(__file__))))) 3 | -------------------------------------------------------------------------------- /example/products/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Product(models.Model): 5 | name = models.CharField("Product name", max_length=255) 6 | value = models.DecimalField(max_digits=11, decimal_places=2) 7 | 8 | def __unicode__(self): 9 | return self.name 10 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_django_admin_report.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/products/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from products.models import Product 3 | 4 | 5 | class ProductAdmin( admin.ModelAdmin ): 6 | # search_fields = ('nome', 'email', 'cpf', 'rg') 7 | list_display = ('name', 'value') 8 | # date_hierarchy = 'data' 9 | # exclude = ('grupo_tributacao',) 10 | admin.site.register( Product, ProductAdmin ) 11 | -------------------------------------------------------------------------------- /example/test_django_admin_report/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_django_admin_report project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_django_admin_report.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /example/orders/migrations/0002_orderitem_total.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('orders', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='orderitem', 16 | name='total', 17 | field=models.DecimalField(default=0, max_digits=11, decimal_places=2), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /example/products/migrations/0002_product_value.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('products', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='product', 16 | name='value', 17 | field=models.DecimalField(default=0, max_digits=11, decimal_places=2), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /example/test_django_admin_report/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.conf import settings 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | # Examples: 8 | # url(r'^$', 'test_django_admin_report.views.home', name='home'), 9 | # url(r'^blog/', include('blog.urls')), 10 | 11 | url(r'^admin/', include(admin.site.urls)), 12 | ) 13 | 14 | if settings.DEBUG: 15 | import debug_toolbar 16 | urlpatterns += patterns('', 17 | url(r'^__debug__/', include(debug_toolbar.urls)), 18 | ) -------------------------------------------------------------------------------- /example/orders/migrations/0003_auto_20141225_2344.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('orders', '0002_orderitem_total'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='orderitem', 16 | name='total', 17 | field=models.DecimalField(default=0, max_digits=11, decimal_places=2, blank=True), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /example/templates/admin/import_export/change_list_export.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load i18n %} 3 | 4 | {# Original template renders object-tools only when has_add_permission is True. #} 5 | {# This hack allows sub templates to add to object-tools #} 6 | {% block object-tools %} 7 | 15 | {% endblock %} -------------------------------------------------------------------------------- /example/products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Product', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('name', models.CharField(max_length=255, verbose_name=b'Product name')), 18 | ], 19 | options={ 20 | }, 21 | bases=(models.Model,), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /example/orders/migrations/0004_orderitemproxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('products', '0002_product_value'), 11 | ('orders', '0003_auto_20141225_2344'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='OrderItemProxy', 17 | fields=[ 18 | ], 19 | options={ 20 | 'verbose_name': 'Order Item', 21 | 'proxy': True, 22 | 'verbose_name_plural': 'Order Items', 23 | }, 24 | bases=('products.product',), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mateus Padua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from setuptools import find_packages 3 | 4 | VERSION = __import__("admin_report").__version__ 5 | 6 | CLASSIFIERS = [ 7 | 'Framework :: Django', 8 | 'Intended Audience :: Developers', 9 | 'License :: OSI Approved :: MIT License', 10 | 'Operating System :: OS Independent', 11 | 'Topic :: Software Development', 12 | 'Programming Language :: Python', 13 | 'Programming Language :: Python :: 2', 14 | 'Programming Language :: Python :: 3', 15 | ] 16 | 17 | install_requires = [] 18 | 19 | setup( 20 | name="django-admin-report", 21 | packages=find_packages(exclude=["tests"]), 22 | description="Django application and library for create reports using all power of the ORM Django", 23 | version=VERSION, 24 | author="Mateus Vanzo de Padua", 25 | author_email="mateuspaduaweb@gmail.com", 26 | license='MIT License', 27 | platforms=['OS Independent'], 28 | url="https://github.com/mateuspadua/django-admin-report", 29 | keywords=['report', 'django', 'admin', 'group_by', 'annotate', 'agregate', 'ORM'], 30 | include_package_data=True, 31 | install_requires=install_requires, 32 | classifiers=CLASSIFIERS, 33 | ) 34 | -------------------------------------------------------------------------------- /example/orders/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models 4 | from products.models import Product 5 | 6 | # Create your models here. 7 | 8 | PAYMENT_TYPE = ( 9 | (1, u"boleto"), 10 | (2, u"cartão de crédito"), 11 | (3, u"débito"), 12 | ) 13 | 14 | GENDER = ( 15 | (0, u"Male"), 16 | (1, u"Female") 17 | ) 18 | 19 | 20 | class Order(models.Model): 21 | data = models.DateField(auto_now_add=True, verbose_name='Data do Cadastro') 22 | payment_type = models.PositiveSmallIntegerField(default=None, choices=PAYMENT_TYPE, blank=True, null=True) 23 | email = models.EmailField(null=True) 24 | delivered = models.BooleanField(default=False) 25 | total_value = models.DecimalField(max_digits=11, decimal_places=2) 26 | gender = models.NullBooleanField(choices=GENDER, null=True, blank=True, default=None) 27 | 28 | def __unicode__(self): 29 | return str(self.id) 30 | 31 | 32 | class OrderItem(models.Model): 33 | class Meta: 34 | verbose_name, verbose_name_plural = u"Order Iten", u"Order Itens" 35 | 36 | order = models.ForeignKey(Order) 37 | product = models.ForeignKey(Product) 38 | quantity = models.PositiveSmallIntegerField(verbose_name=u'quantity sold') 39 | value = models.DecimalField(max_digits=11, decimal_places=2) 40 | total = models.DecimalField(max_digits=11, decimal_places=2, blank=True, default=0) 41 | 42 | def save(self, *args, **kwargs): 43 | self.total = self.quantity * self.value 44 | super(OrderItem, self).save(*args, **kwargs) 45 | 46 | 47 | class OrderProxy(Order): 48 | class Meta: 49 | verbose_name, verbose_name_plural = u"Report Order", u"Report Orders" 50 | proxy = True 51 | 52 | 53 | class ProductProxy(Product): 54 | class Meta: 55 | verbose_name, verbose_name_plural = u"Report Order Item", u"Report Order Items" 56 | proxy = True 57 | -------------------------------------------------------------------------------- /example/orders/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('products', '0002_product_value'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Order', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('data', models.DateField(auto_now_add=True, verbose_name=b'Data do Cadastro')), 19 | ('payment_type', models.PositiveSmallIntegerField(default=None, null=True, blank=True, choices=[(1, 'boleto'), (2, 'cart\xe3o de cr\xe9dito'), (3, 'd\xe9bito')])), 20 | ('email', models.EmailField(max_length=75, null=True)), 21 | ('delivered', models.BooleanField(default=False)), 22 | ('total_value', models.DecimalField(max_digits=11, decimal_places=2)), 23 | ('gender', models.NullBooleanField(default=None, choices=[(0, 'Male'), (1, 'Female')])), 24 | ], 25 | options={ 26 | }, 27 | bases=(models.Model,), 28 | ), 29 | migrations.CreateModel( 30 | name='OrderItem', 31 | fields=[ 32 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 33 | ('quantity', models.PositiveSmallIntegerField(verbose_name='quantity sold')), 34 | ('value', models.DecimalField(max_digits=11, decimal_places=2)), 35 | ('order', models.ForeignKey(to='orders.Order')), 36 | ('product', models.ForeignKey(to='products.Product')), 37 | ], 38 | options={ 39 | 'verbose_name': 'Order Iten', 40 | 'verbose_name_plural': 'Order Itens', 41 | }, 42 | bases=(models.Model,), 43 | ), 44 | migrations.CreateModel( 45 | name='OrderProxy', 46 | fields=[ 47 | ], 48 | options={ 49 | 'verbose_name': 'Order', 50 | 'proxy': True, 51 | 'verbose_name_plural': 'Orders', 52 | }, 53 | bases=('orders.order',), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /example/test_django_admin_report/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for test_django_admin_report project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = '1shaum3ab9sbmfd4*&7re_k3jpu%xqa#r_h1mqct2tx7@=8zh)' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = DEBUG 26 | 27 | ALLOWED_HOSTS = ['*'] 28 | 29 | TEMPLATE_CONTEXT_PROCESSORS = ( 30 | "django.contrib.auth.context_processors.auth", 31 | 'django.core.context_processors.debug', 32 | "django.core.context_processors.i18n", 33 | "django.core.context_processors.media", 34 | "django.core.context_processors.static", 35 | "django.core.context_processors.tz", 36 | "django.contrib.messages.context_processors.messages", 37 | 'django.core.context_processors.request', 38 | ) 39 | 40 | # Application definition 41 | 42 | INSTALLED_APPS = ( 43 | 'admin_report', 44 | 'suit', 45 | 'import_export', 46 | 'django.contrib.admin', 47 | 'django.contrib.auth', 48 | 'django.contrib.contenttypes', 49 | 'django.contrib.sessions', 50 | 'django.contrib.messages', 51 | 'django.contrib.staticfiles', 52 | 53 | 'orders', 54 | 'products', 55 | 'django_extensions', 56 | 'debug_toolbar', 57 | ) 58 | 59 | DEBUG_TOOLBAR_PATCH_SETTINGS = False 60 | INTERNAL_IPS = "127.0.0.1" 61 | 62 | MIDDLEWARE_CLASSES = ( 63 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 64 | 'django.contrib.sessions.middleware.SessionMiddleware', 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.middleware.csrf.CsrfViewMiddleware', 67 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 68 | 'django.contrib.messages.middleware.MessageMiddleware', 69 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 70 | ) 71 | 72 | ROOT_URLCONF = 'test_django_admin_report.urls' 73 | 74 | WSGI_APPLICATION = 'test_django_admin_report.wsgi.application' 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 79 | 80 | DATABASES = { 81 | 'default': { 82 | 'ENGINE': 'django.db.backends.mysql', 83 | 'NAME': 'django_chart_report', 84 | 'USER': 'root', 85 | 'PASSWORD': '123456', 86 | 'HOST': '', 87 | 'PORT': '', 88 | } 89 | } 90 | 91 | # Internationalization 92 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 93 | 94 | LANGUAGE_CODE = 'en-us' 95 | 96 | TIME_ZONE = 'UTC' 97 | 98 | USE_I18N = True 99 | 100 | USE_L10N = True 101 | 102 | USE_TZ = True 103 | 104 | 105 | # Static files (CSS, JavaScript, Images) 106 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 107 | 108 | STATIC_URL = '/static/' 109 | 110 | TEMPLATE_DIRS = ( 111 | os.path.join(BASE_DIR, 'templates'), 112 | ) 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-admin-report 2 | Create reports using the full potential of django admin 3 | 4 | Django-Admin-Report 5 | ============== 6 | 7 | This is a [Django](https://www.djangoproject.com/) app project 8 | 9 | The app includes admin_report Mixin. 10 | 11 | Installation 12 | ============ 13 | 14 | 1. Install `django_admin_report` 15 | 16 | pip install django_admin_report 17 | 18 | 2. Add `admin_report` to your `INSTALLED_APPS` in your project settings in firstly. Example: 19 | 20 | INSTALLED_APPS = ( 21 | 'admin_report', # firstly 22 | 'import_export', 23 | 'django.contrib.admin', 24 | ... 25 | ) 26 | 27 | Documentation 28 | ============ 29 | 30 | How use: 31 | 32 | 1. First: look this model.py. This example is inside this project in the "example" folder 33 | 34 | class Product(models.Model): 35 | name = models.CharField("Product name", max_length=255) 36 | value = models.DecimalField(max_digits=11, decimal_places=2) 37 | 38 | def __unicode__(self): 39 | return self.name 40 | 41 | 42 | class Order(models.Model): 43 | data = models.DateField(auto_now_add=True, verbose_name='Data do Cadastro') 44 | payment_type = models.PositiveSmallIntegerField(default=None, choices=PAYMENT_TYPE, blank=True, null=True) 45 | email = models.EmailField(null=True) 46 | delivered = models.BooleanField(default=False) 47 | total_value = models.DecimalField(max_digits=11, decimal_places=2) 48 | gender = models.NullBooleanField(choices=GENDER, null=True, blank=True, default=None) 49 | 50 | def __unicode__(self): 51 | return str(self.id) 52 | 53 | 54 | class OrderItem(models.Model): 55 | class Meta: 56 | verbose_name, verbose_name_plural = u"Order Iten", u"Order Itens" 57 | 58 | order = models.ForeignKey(Order) 59 | product = models.ForeignKey(Product) 60 | quantity = models.PositiveSmallIntegerField(verbose_name=u'quantity sold') 61 | value = models.DecimalField(max_digits=11, decimal_places=2) 62 | total = models.DecimalField(max_digits=11, decimal_places=2, blank=True, default=0) 63 | 64 | def save(self, *args, **kwargs): 65 | self.total = self.quantity * self.value 66 | super(OrderItem, self).save(*args, **kwargs) 67 | 68 | 69 | 2. Second: in your app, add in your model.py one proxy model to model that you desired create report, like this: 70 | 71 | class ProductProxy(Product): 72 | class Meta: 73 | verbose_name, verbose_name_plural = u"Report Order Item", u"Report Order Items" 74 | proxy = True 75 | 76 | 3. Third: in your admin.py create one Admin using ChartReportAdmin 77 | 78 | from django.db.models import Sum, Avg, Count, Min, Max 79 | from admin_report.mixins import ChartReportAdmin 80 | 81 | ..... 82 | 83 | class ReportOrderItemsAdmin(ChartReportAdmin): 84 | list_display = ('name', 'orderitem__value__avg', 'orderitem__value__max', 'orderitem__value__min', 'orderitem__quantity__sum', 'orderitem__total__sum',) 85 | 86 | report_annotates = ( 87 | ("orderitem__quantity", Sum, "subtotal total items sold"), 88 | ("orderitem__total", Sum, "subtotal total value sold"), 89 | ("orderitem__value", Avg, "product sold average"), 90 | ("orderitem__value", Max, "higher sold value"), 91 | ("orderitem__value", Min, "lower sold value"), 92 | ) 93 | 94 | report_aggregates = ( 95 | ('orderitem__total__sum', Sum, "Total: R$ %value"), 96 | ('orderitem__quantity', Sum, "total items sold"), 97 | ) 98 | 99 | admin.site.register( ProductProxy, ReportOrderItemsAdmin ) 100 | 101 | 4. Fourth: More details about properties, soon. -------------------------------------------------------------------------------- /example/orders/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | from .models import * 5 | from django.db.models import Sum, Avg, Count, Min, Max 6 | from admin_report.mixins import ChartReportAdmin 7 | from import_export import resources 8 | from import_export.admin import ExportMixin 9 | from import_export import fields 10 | from django.utils import formats 11 | 12 | 13 | class AdminNoAddPermissionMixin(object): 14 | def has_add_permission(self, request): 15 | return False 16 | 17 | 18 | class OrderItemInline(admin.TabularInline): 19 | model = OrderItem 20 | extra = 1 21 | 22 | 23 | class OrderAdmin( admin.ModelAdmin ): 24 | # search_fields = ('nome', 'email', 'cpf', 'rg') 25 | list_display = ('data', 'payment_type', 'email', 'delivered', 'total_value', 'gender') 26 | list_filter = ('data', 'payment_type') 27 | inlines = [OrderItemInline] 28 | # date_hierarchy = 'data' 29 | # exclude = ('grupo_tributacao',) 30 | admin.site.register( Order, OrderAdmin ) 31 | 32 | 33 | class ReportOrderAdmin(ExportMixin, AdminNoAddPermissionMixin, ChartReportAdmin): 34 | list_filter = ['payment_type', 'email', 'delivered'] 35 | list_display = ('data', 'payment_type', 'email', 'delivered', 'total_value', 'gender') 36 | 37 | # group_by = "gender" 38 | 39 | report_aggregates = ( 40 | ('total_value', Sum, "Total vendido"), 41 | ('total_value', Avg, "Valor médio"), 42 | ('total_value', Count, "Número de pedidos"), 43 | ) 44 | 45 | admin.site.register( OrderProxy, ReportOrderAdmin ) 46 | 47 | 48 | class ReportOrderItemsResource(resources.ModelResource): 49 | orderitem__value__avg = fields.Field(attribute="orderitem__value__avg", column_name="valor médio do produto") 50 | orderitem__value__max = fields.Field(attribute="orderitem__value__max", column_name="maior valor de venda") 51 | orderitem__value__min = fields.Field(attribute="orderitem__value__min", column_name="menor valor de venda") 52 | orderitem__quantity__sum = fields.Field(attribute="orderitem__quantity__sum", column_name="total de itens vendidos") 53 | orderitem__total__sum = fields.Field(attribute="orderitem__total__sum", column_name="valor total vendido") 54 | 55 | class Meta: 56 | model = ProductProxy 57 | fields = ('name', 'orderitem__value__avg', 'orderitem__value__max', 'orderitem__value__min', 'orderitem__quantity__sum', 'orderitem__total__sum',) 58 | export_order = fields 59 | 60 | def dehydrate_orderitem__value__avg(self, obj): 61 | return u"R$ {0}".format(formats.number_format(obj.orderitem__value__avg, 2)) 62 | 63 | def dehydrate_orderitem__value__max(self, obj): 64 | return formats.number_format(obj.orderitem__value__max, 2) 65 | 66 | def dehydrate_orderitem__value__min(self, obj): 67 | return formats.number_format(obj.orderitem__value__min, 2) 68 | 69 | def dehydrate_orderitem__quantity__sum(self, obj): 70 | return formats.number_format(obj.orderitem__quantity__sum, 2) 71 | 72 | def dehydrate_orderitem__total__sum(self, obj): 73 | return formats.number_format(obj.orderitem__total__sum, 2) 74 | 75 | 76 | class ReportOrderItemsAdmin(ExportMixin, AdminNoAddPermissionMixin, ChartReportAdmin): 77 | resource_class = ReportOrderItemsResource 78 | list_filter = ['name', 'orderitem__order__payment_type', ] 79 | list_display = ('name', 'valor_atual', 'orderitem__value__avg', 'orderitem__value__max', 'orderitem__value__min', 'orderitem__quantity__sum', 'orderitem__total__sum',) 80 | 81 | def valor_atual(self, obj): 82 | return obj.value 83 | valor_atual.short_description = 'valor atual do produto' 84 | valor_atual.admin_order_field = 'value' 85 | # valor_atual.allow_tags = True 86 | 87 | report_annotates = ( 88 | ("orderitem__quantity", Sum, "total de itens vendidos"), 89 | ("orderitem__total", Sum, "valor total vendido"), 90 | # ("orderitem__total", Count, "count total vendido"), 91 | ("orderitem__value", Avg, "valor médio do produto"), 92 | ("orderitem__value", Max, "maior valor de venda"), 93 | ("orderitem__value", Min, "menor valor de venda"), 94 | # ("orderitem__valor_sem_desconto", Min, "menor valor do produto"), 95 | ) 96 | 97 | report_aggregates = ( 98 | ('orderitem__total__sum', Sum, "Total: R$ %value"), 99 | ('orderitem__quantity', Sum, "total de itens vendidos"), 100 | ) 101 | 102 | admin.site.register( ProductProxy, ReportOrderItemsAdmin ) 103 | -------------------------------------------------------------------------------- /admin_report/templates/admin/change_list_results.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | {% load i18n admin_static %}{% load cycle from future %} 3 | {% if result_hidden_fields %} 4 |
{# DIV for HTML validation #} 5 | {% for item in result_hidden_fields %}{{ item }}{% endfor %} 6 |
7 | {% endif %} 8 | {% if results %} 9 |
10 | 11 | 12 | 13 | {% for header in result_headers %} 14 | {% endfor %} 27 | 28 | 29 | 30 | {% if cl.result_aggregate %} 31 | 32 | 33 | {% for item in cl.result_aggregate %} 34 | 35 | {% endfor %} 36 | 37 | 38 | {% endif %} 39 | 40 | 41 | {% for result in results %} 42 | {% if result.form.non_field_errors %} 43 | 44 | {% endif %} 45 | {% for item in result %}{{ item }}{% endfor %} 46 | {% endfor %} 47 | 48 |
15 | {% if header.sortable %} 16 | {% if header.sort_priority > 0 %} 17 |
18 | 19 | {% if num_sorted_fields > 1 %}{{ header.sort_priority }}{% endif %} 20 | 21 |
22 | {% endif %} 23 | {% endif %} 24 |
{% if header.sortable %}{{ header.text|capfirst }}{% else %}{{ header.text|capfirst }}{% endif %}
25 |
26 |
{{ item|safe }}
{{ result.form.non_field_errors }}
49 |
50 | {% endif %} 51 | {% endcomment %} 52 | 53 | {% load i18n admin_static suit_list %} 54 | {% if result_hidden_fields %} 55 |
{# DIV for HTML validation #} 56 | {% for item in result_hidden_fields %}{{ item }}{% endfor %} 57 |
58 | {% endif %} 59 | {% if results %} 60 | {% if cl.result_aggregate %} 61 |
67 | Resumo relatório 73 | {% for item in cl.result_aggregate %} 74 | {% if item %} 75 | {{ item|safe }}
76 | {% endif %} 77 | {% endfor %} 78 |
79 | {% endif %} 80 |
81 | 82 | 83 | 84 | {% for header in result_headers|headers_handler:cl %} 85 | {% endfor %} 103 | 104 | 105 | 106 | {% if cl.result_aggregate_by_column %} 107 | {% if cl.paginator.num_pages == cl.page_num|add:"1" or cl.show_all %} 108 | 109 | 110 | {% for item in cl.result_aggregate_by_column %} 111 | 112 | {% endfor %} 113 | 114 | 115 | {% endif %} 116 | {% endif %} 117 | 118 | {% for result in results|cells_handler:cl %} 119 | {% if result.form.non_field_errors %} 120 | 121 | 122 | 123 | {% endif %} 124 | 125 | {% for item in result %}{{ item }}{% endfor %} 126 | {% endfor %} 127 | 128 |
86 | {% if header.sortable %} 87 | {% if header.sort_priority > 0 %} 88 |
89 |
90 | {% if num_sorted_fields > 1 %} 91 | {{ header.sort_priority }}{% endif %} 92 | 93 | 95 |
96 |
97 | {% endif %} 98 | {% endif %} 99 |
{% if header.sortable %} 100 | {{ header.text|capfirst }}{% else %}{{ header.text|capfirst }}{% endif %} 101 |
102 |
{{ item|safe }}
{{ result.form.non_field_errors }}
129 |
130 | {% endif %} 131 | -------------------------------------------------------------------------------- /admin_report/mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | import types 4 | import sys 5 | # from django.contrib.admin.validation import ModelAdminValidator 6 | # from django.db.models import ForeignKey 7 | # from django.db.models.constants import LOOKUP_SEP 8 | # from django.db.models.sql.constants import QUERY_TERMS 9 | from django.contrib import admin 10 | from django.contrib.admin.views.main import ChangeList 11 | from django.db.models import Sum, Avg, Count, Max, Min, F 12 | from django.http import HttpResponseRedirect 13 | from django.utils import formats 14 | 15 | map_aggregates = ((Sum, "__sum"), (Count, "__count"), (Avg, "__avg"), (Max, "__max"), (Min, "__min"), (F, "__expression")) 16 | 17 | 18 | def function_builder(name_function, attr, title_column): 19 | def new_function(self, obj): 20 | return getattr(obj, attr) 21 | new_function.__name__ = name_function 22 | if title_column: 23 | new_function.short_description = title_column 24 | new_function.admin_order_field = attr 25 | new_function.allow_tags = True 26 | return new_function 27 | 28 | 29 | class ChangeListChartReport(ChangeList): 30 | 31 | result_aggregate = [] 32 | 33 | # def get_ordering(self, request, queryset): 34 | # if self.model_admin.annotate_fields: 35 | # return [] 36 | # else: 37 | # return super(ChangeListChartReport, self).get_ordering(request, queryset) 38 | 39 | def get_queryset(self, request): 40 | 41 | qs = super(ChangeListChartReport, self).get_queryset(request) 42 | 43 | if self.model_admin.annotate_fields: 44 | # qs = self.root_queryset 45 | 46 | # estudar essa parte, esta estranho 47 | if self.model_admin.group_by: 48 | qs = qs.values(*self.model_admin.group_by) 49 | 50 | # faz uma copia antes de chamar o metodo annotate, pois para campos aggregate que não 51 | # representam um campo annotate, não se pode ter o annotate na query 52 | self.query_to_normal_aggregate = qs 53 | 54 | # print "self.model_admin.annotate_fields" 55 | # print self.model_admin.annotate_fields 56 | 57 | # qs = qs.annotate(*self.model_admin.annotate_fields_2, **self.model_admin.annotate_fields) 58 | qs = qs.annotate(**self.model_admin.annotate_fields) 59 | 60 | # Set ordering. 61 | ordering = self.get_ordering(request, qs) 62 | if self.model_admin.group_by: 63 | # muito esquisito isso, mas depois de muito estudo descobri que a ordem 'pk' ou '-pk' 64 | # que é adicionado por padrão pela função acima 'get_ordering' atrapalha 65 | # o GROUP_BY, como descrito um pouco no link abaixo 66 | # https://docs.djangoproject.com/en/dev/topics/db/aggregation/#aggregation-ordering-interaction 67 | if '-pk' in ordering: 68 | ordering.remove('-pk') 69 | if 'pk' in ordering: 70 | ordering.remove('pk') 71 | qs = qs.order_by(*ordering) 72 | 73 | # Apply search results 74 | qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query) 75 | 76 | # print qs.query 77 | # print "get_queryset" 78 | else: 79 | self.query_to_normal_aggregate = qs 80 | 81 | # print "#### query final ####" 82 | # print qs.query 83 | 84 | return qs 85 | 86 | def get_results(self, request): 87 | super(self.__class__, self).get_results(request) 88 | 89 | # print self.result_list[0].pedidoitem__valor_unitario_sem_descontos__avg 90 | # print self.result_list[0].pedidoitem__valor_unitario_sem_descontos__max 91 | 92 | # print "get_results" 93 | # print self.queryset.query 94 | 95 | if self.result_list and isinstance(self.result_list[0], dict): 96 | pk_name = self.model._meta.pk.name 97 | try: 98 | ids = [i[pk_name] for i in self.result_list] 99 | except KeyError: 100 | ids = [] 101 | 102 | result_list_in_model = self.model.objects.filter(pk__in=ids) 103 | result_list_in_model_dict = {} 104 | for item_model in result_list_in_model: 105 | result_list_in_model_dict[getattr(item_model, pk_name)] = item_model 106 | 107 | # print "result_list_in_model_dict =========" 108 | # print result_list_in_model_dict 109 | 110 | new_result_list = [] 111 | for row in self.result_list: 112 | if pk_name in row and result_list_in_model_dict: 113 | new_row = copy.deepcopy(result_list_in_model_dict[row[pk_name]]) 114 | else: 115 | new_row = self.model() 116 | 117 | for key, value in list(row.items()): 118 | # if hasattr(new_row, key): 119 | # print self.model._meta.get_field(key) 120 | # field_object, model, direct, m2m = self.model._meta.get_field_by_name(key) 121 | # if (m2m or isinstance(field_object, ForeignKey)) is True: 122 | # continue 123 | 124 | setattr(new_row, key, value) 125 | 126 | new_result_list.append(new_row) 127 | 128 | self.result_list = new_result_list 129 | 130 | self.get_result_aggregate() 131 | 132 | def get_result_aggregate(self): 133 | # print self.list_display 134 | 135 | result_aggregate = [] 136 | result_aggregate_by_column = [] 137 | result_aggregate_queryset = None 138 | result_aggregate_from_normal_queryset = {} 139 | result_aggregate_from_annotate_queryset = {} 140 | # from django.db import connection 141 | # from django.db import reset_queries 142 | # reset_queries() 143 | 144 | # print "===== self.model_admin.aggregate_fields_from_normal" 145 | # print self.model_admin.aggregate_fields_from_normal 146 | 147 | # print "===== aggregate_fields_from_annotate" 148 | # print self.model_admin.aggregate_fields_from_annotate 149 | 150 | qs = self.queryset 151 | if self.model_admin.aggregate_fields_from_normal: 152 | # print "normal" 153 | result_aggregate_from_normal_queryset = self.query_to_normal_aggregate.aggregate(*self.model_admin.aggregate_fields_from_normal) 154 | 155 | if self.model_admin.aggregate_fields_from_annotate: 156 | # print "annotate" 157 | result_aggregate_from_annotate_queryset = qs.aggregate(*self.model_admin.aggregate_fields_from_annotate) 158 | # print "########## result_aggregate_from_annotate_queryset ##########" 159 | # print result_aggregate_from_annotate_queryset 160 | 161 | # print "#######" 162 | # print qs.query 163 | # print connection.queries 164 | 165 | # print result_aggregate_from_normal_queryset.query 166 | result_aggregate_queryset = dict(result_aggregate_from_normal_queryset, **result_aggregate_from_annotate_queryset) 167 | 168 | print result_aggregate_queryset 169 | 170 | def get_result_aggregate(aggregate): 171 | # clean_name_field = aggregate[0][:-len(aggregate[0][aggregate[0].rfind("__"):])] 172 | pos_value_place_holder = aggregate[2].find("%value") 173 | 174 | aggregate_string_replace = "{0} {1}" 175 | if pos_value_place_holder != -1: 176 | aggregate_string_replace = aggregate[2].replace("%value", "{1}") 177 | 178 | if isinstance(result_aggregate_queryset[aggregate[0]], float): 179 | label_foot = formats.number_format(result_aggregate_queryset[aggregate[0]], 2) 180 | else: 181 | label_foot = formats.localize(result_aggregate_queryset[aggregate[0]], use_l10n=True) 182 | 183 | label_foot = aggregate_string_replace.format(aggregate[2], label_foot) 184 | # result_aggregate_temp = label_foot 185 | return label_foot 186 | 187 | if result_aggregate_queryset: 188 | for column in self.list_display: 189 | result_aggregate_temp = [] 190 | if column in self.model_admin.map_list_display_and_aggregate: 191 | # print "column: ", column 192 | for aggregate in self.model_admin.map_list_display_and_aggregate[column]: 193 | # print aggregate 194 | result_aggregate_temp.append(get_result_aggregate(aggregate)) 195 | 196 | if result_aggregate_temp: 197 | result_aggregate_by_column.append("
".join(result_aggregate_temp)) 198 | else: 199 | result_aggregate_by_column.append("") 200 | 201 | for aggregate in self.model_admin.map_summary_aggregate: 202 | result_aggregate.append(get_result_aggregate(aggregate)) 203 | 204 | # print "################### result_aggregate #####################" 205 | # print result_aggregate 206 | 207 | # print "################### result_aggregate_by_column #####################" 208 | # print result_aggregate_by_column 209 | 210 | self.result_aggregate = result_aggregate 211 | self.result_aggregate_by_column = result_aggregate_by_column 212 | 213 | 214 | # class ModelAdminValidator2(ModelAdminValidator): 215 | # def validate_list_display(self, cls, model): 216 | # # super(ModelAdminValidator2, self).validate_list_display(cls, model) 217 | # pass 218 | 219 | 220 | class AdminExceptionFieldsFilterMixin(admin.ModelAdmin): 221 | 222 | exception_fields_filter = () 223 | 224 | def lookup_allowed(self, lookup, value): 225 | # ret = super(AdminExceptionFieldsFilterMixin, self).lookup_allowed(lookup, value) 226 | # if not ret and self.exception_fields_filter: 227 | # parts = lookup.split(LOOKUP_SEP) 228 | 229 | # if len(parts) > 1 and parts[-1] in QUERY_TERMS: 230 | # parts.pop() 231 | # clean_lookup = LOOKUP_SEP.join(parts) 232 | # if clean_lookup in self.exception_fields_filter: 233 | # return True 234 | # return ret 235 | return True 236 | 237 | 238 | class ChartReportAdmin(admin.ModelAdmin): 239 | # change_list_template = "admin/relatorios/change_list.html" 240 | 241 | report_annotates = () 242 | report_aggregates = () 243 | group_by = () 244 | list_display_links = None 245 | # validator_class = ModelAdminValidator2 246 | 247 | # def __new__(cls, *args, **kwargs): 248 | # return super(ChartReportAdmin, cls).__new__(cls, *args, **kwargs) 249 | 250 | class Media: 251 | css = { 252 | 'all': ('admin/css/django_admin_report/django_admin_report.css',) 253 | } 254 | 255 | def __init__(self, model, admin_site): 256 | # print "########################### report_annotates ###############################" 257 | # print self.report_annotates 258 | self.annotate_fields = {} 259 | self.aggregate_fields_from_normal = [] 260 | self.aggregate_fields_from_annotate = [] 261 | self.map_list_display_and_aggregate = {} 262 | self.map_summary_aggregate = [] 263 | 264 | for annotate in self.report_annotates: 265 | for func, end_field_name in map_aggregates: 266 | if func == annotate[1]: 267 | name_field_annotate = "{}{}".format(annotate[0], end_field_name) 268 | self.annotate_fields.update({name_field_annotate: annotate[1](annotate[0])}) 269 | self.addMethod(function_builder(name_field_annotate, name_field_annotate, annotate[2] if len(annotate) == 3 else None)) 270 | break 271 | 272 | for aggregate in self.report_aggregates: 273 | for func, end_field_name in map_aggregates: 274 | if func == aggregate[1]: 275 | copy_aggregate = list(aggregate[:]) 276 | name_field_aggregate = "{0}{1}".format(aggregate[0], end_field_name) 277 | 278 | copy_aggregate[0] = name_field_aggregate 279 | 280 | new_label = "{0}{1}".format(aggregate[0], end_field_name) 281 | if len(copy_aggregate) == 2: 282 | copy_aggregate.append(new_label) 283 | elif not copy_aggregate[2]: 284 | copy_aggregate[2] = new_label 285 | 286 | if len(copy_aggregate) == 4: # significa que foi especificado em qual coluna deve aparecer o valor agregado 287 | column_display_list = aggregate[3] 288 | else: 289 | column_display_list = aggregate[0] 290 | if column_display_list not in self.list_display: 291 | column_display_list = new_label 292 | 293 | if column_display_list not in self.map_list_display_and_aggregate: 294 | self.map_list_display_and_aggregate[column_display_list] = [] 295 | 296 | self.map_list_display_and_aggregate[column_display_list].append(copy_aggregate) 297 | self.map_summary_aggregate.append(copy_aggregate) 298 | break 299 | 300 | if aggregate[0] in self.annotate_fields: 301 | self.aggregate_fields_from_annotate.append(aggregate[1](aggregate[0])) 302 | else: 303 | self.aggregate_fields_from_normal.append(aggregate[1](aggregate[0])) 304 | 305 | # print self.aggregate_fields_from_normal 306 | # print self.aggregate_fields_from_annotate 307 | # print "########################## self.map_list_display_and_aggregate #########################" 308 | # print self.map_list_display_and_aggregate 309 | 310 | self.list_max_show_all = sys.maxsize 311 | 312 | super(ChartReportAdmin, self).__init__(model, admin_site) 313 | 314 | # TODO: refazer esse metodo, nao esta legal 315 | def add_view(self, request, form_url='', extra_context=None): 316 | return HttpResponseRedirect(request.META["HTTP_REFERER"]) # volta sempre para a mesma pagina 317 | 318 | def addMethod(self, func): 319 | return setattr(self, func.__name__, types.MethodType(func, self)) 320 | 321 | def get_changelist(self, request, **kwargs): 322 | return ChangeListChartReport 323 | --------------------------------------------------------------------------------