diff --git a/jarbas/dashboard/admin/__init__.py b/jarbas/dashboard/admin/__init__.py
index c4cbd4f07..317c4d021 100644
--- a/jarbas/dashboard/admin/__init__.py
+++ b/jarbas/dashboard/admin/__init__.py
@@ -1,311 +1,26 @@
-from decimal import Decimal, InvalidOperation
-from hashlib import md5
-
-from brazilnum.cnpj import format_cnpj
-from brazilnum.cpf import format_cpf
-from django.contrib.postgres.search import SearchQuery, SearchRank
-from django.core.cache import cache
-from django.db.models import Count, F, Sum
-from django.db.models.functions import Concat
-from django.utils.safestring import mark_safe
-
from jarbas.chamber_of_deputies.models import (
- Reimbursement,
+ Reimbursement as ChamberOfDeputiesReimbursement,
ReimbursementSummary,
- SocialMedia,
)
-from jarbas.chamber_of_deputies.serializers import clean_cnpj_cpf
-from jarbas.dashboard.admin import list_filters, widgets
-from jarbas.dashboard.admin.paginators import CachedCountPaginator
-from jarbas.dashboard.admin.subquotas import Subquotas
-from jarbas.public_admin.admin import PublicAdminModelAdmin
-from jarbas.public_admin.sites import public_admin
-
-
-ALL_FIELDS = sorted(Reimbursement._meta.fields, key=lambda f: f.verbose_name)
-CUSTOM_WIDGETS = ('receipt_url', 'subquota_description', 'suspicions')
-READONLY_FIELDS = (f.name for f in ALL_FIELDS if f.name not in CUSTOM_WIDGETS)
-
-
-class ReimbursementModelAdmin(PublicAdminModelAdmin):
-
- list_display = (
- 'short_document_id',
- 'jarbas',
- 'social_profile',
- 'rosies_tweet',
- 'receipt_link',
- 'congressperson_name',
- 'year',
- 'subquota_translated',
- 'supplier_info',
- 'value',
- 'suspicious'
- )
-
- search_fields = ('search_vector',)
-
- list_filter = (
- list_filters.SuspiciousListFilter,
- list_filters.HasReceiptFilter,
- list_filters.HasReimbursementNumberFilter,
- list_filters.StateListFilter,
- list_filters.YearListFilter,
- list_filters.MonthListFilter,
- list_filters.DocumentTypeListFilter,
- list_filters.SubquotaListFilter,
- )
-
- fields = tuple(f.name for f in ALL_FIELDS)
- readonly_fields = tuple(READONLY_FIELDS)
- list_select_related = ('tweet',)
- paginator = CachedCountPaginator
-
- def _format_document(self, obj):
- if obj.cnpj_cpf:
- if len(obj.cnpj_cpf) == 14:
- return format_cnpj(obj.cnpj_cpf)
-
- if len(obj.cnpj_cpf) == 11:
- return format_cpf(obj.cnpj_cpf)
-
- return obj.cnpj_cpf
-
- def supplier_info(self, obj):
- return mark_safe(f'{obj.supplier}
{self._format_document(obj)}')
-
- supplier_info.short_description = 'Fornecedor'
-
- def jarbas(self, obj):
- base_url = '/layers/#/documentId/{}/'
- url = base_url.format(obj.document_id)
- image = ''
- return mark_safe('{}'.format(url, image))
-
- jarbas.short_description = ''
-
- def social_profile(self, obj):
- social_media = SocialMedia.objects.filter(congressperson_id=obj.congressperson_id).first()
- if not social_media:
- return ''
-
- tw_link = ''
- tw_img = '/static/image/twitter-icon.png'
- tw_profile = social_media.twitter_profile or social_media.secondary_twitter_profile
- if tw_profile:
- tw_link = ''.format(
- tw_profile, tw_img
- )
-
- fb_link = ''
- fb_img = '/static/image/facebook-icon.png'
- if social_media.facebook_page:
- fb_link = ''.format(
- social_media.facebook_page, fb_img
- )
-
- return mark_safe(f'{tw_link} {fb_link}')
-
- social_profile.short_description = 'Social'
-
- def rosies_tweet(self, obj):
- try:
- return mark_safe('🤖'.format(obj.tweet.get_url()))
- except Reimbursement.tweet.RelatedObjectDoesNotExist:
- return ''
-
- rosies_tweet.short_description = ''
-
- def receipt_link(self, obj):
- if not obj.receipt_url:
- return ''
- image = ''
- return mark_safe('{}'.format(obj.receipt_url, image))
-
- receipt_link.short_description = ''
-
- def suspicious(self, obj):
- return obj.suspicions is not None
-
- suspicious.short_description = 'suspeito'
- suspicious.boolean = True
-
- def has_receipt_url(self, obj):
- return obj.receipt_url is not None
-
- has_receipt_url.short_description = 'recibo'
- has_receipt_url.boolean = True
-
- def value(self, obj):
- return 'R$ {:.2f}'.format(obj.total_net_value).replace('.', ',')
-
- value.short_description = 'valor'
- value.admin_order_field = 'total_net_value'
-
- def short_document_id(self, obj):
- return obj.document_id
-
- short_document_id.short_description = 'Reembolso'
-
- def subquota_translated(self, obj):
- return Subquotas.pt_br(obj.subquota_description)
-
- def get_object(self, request, object_id, from_field=None):
- obj = super().get_object(request, object_id, from_field)
- if obj and not obj.receipt_fetched:
- obj.get_receipt_url()
- return obj
-
- def formfield_for_dbfield(self, db_field, **kwargs):
- if db_field.name in CUSTOM_WIDGETS:
- custom_widgets = dict(
- subquota_description=widgets.SubquotaWidget,
- receipt_url=widgets.ReceiptUrlWidget,
- suspicions=widgets.SuspiciousWidget
- )
- kwargs['widget'] = custom_widgets.get(db_field.name)
- return super().formfield_for_dbfield(db_field, **kwargs)
-
- def get_search_results(self, request, queryset, search_term):
- queryset, distinct = super(ReimbursementModelAdmin, self) \
- .get_search_results(request, queryset, None)
-
- if search_term:
- # if a cnpj/cpf strip characters other than digits
- search_term = clean_cnpj_cpf(search_term)
- query = SearchQuery(search_term, config='portuguese')
- rank = SearchRank(F('search_vector'), query)
- queryset = queryset.annotate(rank=rank).filter(search_vector=query)
-
- if not queryset.was_ordered():
- queryset.order_by('-rank')
-
- return queryset, distinct
-
-
-class ReimbursementSummaryModelAdmin(PublicAdminModelAdmin):
- change_list_template = 'dashboard/reimbursement_summary_change_list.html'
- list_filter = (
- list_filters.SuspiciousListFilter,
- list_filters.HasReimbursementNumberFilter,
- list_filters.StateListFilter,
- list_filters.YearListFilter,
- list_filters.MonthListFilter,
- list_filters.DocumentTypeListFilter,
- )
-
- def get_chart_grouping(self, request):
- """Depending on the year selected on the sidebar filters, returns the
- grouping criteria for the bottom bar chart:
- * if user is seeing a page with no year filter, the chart shows
- reimbursements grouped by year
- * if the user is seeing a page filtered by a specific year, the chart
- shows reimbursements grouped by month
- """
- if 'year' in request.GET:
- return 'month'
- return 'year'
-
- @staticmethod
- def serialize_summary_over_time(row, minimum_percentage='0.05', **kwargs):
- low = kwargs.get('low') or Decimal('0')
- high = kwargs.get('high') or Decimal('0')
- chart_grouping = kwargs.get('chart_grouping')
- chart_grouping_key = kwargs.get('chart_grouping_key')
- minimum_percentage = Decimal(minimum_percentage)
- total = row['total']
-
- try:
- percentage = (total - low) / (high - low)
- except InvalidOperation:
- percentage = Decimal('0')
-
- ratio = Decimal('1') - minimum_percentage
- corrected_percentage = minimum_percentage + (ratio * percentage)
- bar_height = Decimal('100') * corrected_percentage
-
- return {
- 'label': chart_grouping,
- 'chart_grouping': row[chart_grouping_key],
- 'total': row['total'] or 0,
- 'percent': bar_height
- }
-
- def get_cached_context(self, request, queryset):
- url = request.build_absolute_uri()
- hashed = md5(url.encode('utf-8')).hexdigest()
- key = f'cached_reimbursement_summary_context_{hashed}'
- context = cache.get(key)
-
- if context is not None:
- return context
-
- metrics = {
- 'total_reimbursements': Count('id'),
- 'total_value': Sum('total_net_value'),
- }
- queryset = (
- queryset
- .values('subquota_description')
- .annotate(**metrics)
- .order_by('-total_value')
- )
-
- chart_grouping = self.get_chart_grouping(request)
- if chart_grouping == 'year':
- chart_grouping_key = 'year'
- summary_over_time = (
- queryset
- .values('year')
- .annotate(total=Sum('total_net_value'))
- .order_by('year')
- )
- else:
- chart_grouping_key = 'chart_grouping'
- summary_over_time = (
- queryset
- .annotate(chart_grouping=Concat('year', 'month'))
- .values('chart_grouping')
- .annotate(total=Sum('total_net_value'))
- .order_by('year', 'month')
- )
-
- summary_over_time = tuple(summary_over_time)
- totals = tuple(row['total'] for row in summary_over_time)
- over_time_args = {
- 'chart_grouping': chart_grouping,
- 'chart_grouping_key': chart_grouping_key,
- 'low': min(totals, default=0),
- 'high': max(totals, default=0)
- }
-
- context = {
- 'year': request.GET.get('year'),
- 'month': request.GET.get('month'),
- 'chart_grouping': chart_grouping,
- 'summary': tuple(queryset),
- 'summary_total': dict(queryset.aggregate(**metrics)),
- 'summary_over_time': tuple(
- self.serialize_summary_over_time(row, **over_time_args)
- for row in summary_over_time
- )
- }
-
- cache.set(key, context, 60 * 60 * 6)
- return context
-
- def changelist_view(self, request, extra=None):
- response = super().changelist_view(request, extra_context=extra)
-
- try:
- queryset = response.context_data['cl'].queryset
- except (AttributeError, KeyError):
- return response
-
- context = self.get_cached_context(request, queryset)
- response.context_data.update(context)
- return response
+from jarbas.federal_senate.models import (
+ Reimbursement as FederalSenateReimbursement,
+)
+from jarbas.dashboard.admin.chamber_of_deputies import (
+ ChamberOfDeputiesReimbursementModelAdmin,
+ ReimbursementSummaryModelAdmin
+)
+from jarbas.dashboard.admin.federal_senate import (
+ FederalSenateReimbursementModelAdmin,
+)
+from jarbas.public_admin.sites import public_admin
-public_admin.register(Reimbursement, ReimbursementModelAdmin)
+public_admin.register(
+ ChamberOfDeputiesReimbursement,
+ ChamberOfDeputiesReimbursementModelAdmin
+)
+public_admin.register(
+ FederalSenateReimbursement,
+ FederalSenateReimbursementModelAdmin
+)
public_admin.register(ReimbursementSummary, ReimbursementSummaryModelAdmin)
diff --git a/jarbas/dashboard/admin/chamber_of_deputies.py b/jarbas/dashboard/admin/chamber_of_deputies.py
new file mode 100644
index 000000000..3f8ff52e4
--- /dev/null
+++ b/jarbas/dashboard/admin/chamber_of_deputies.py
@@ -0,0 +1,279 @@
+from decimal import Decimal, InvalidOperation
+from hashlib import md5
+
+from django.contrib.postgres.search import SearchQuery, SearchRank
+from django.core.cache import cache
+from django.db.models import Count, F, Sum
+from django.db.models.functions import Concat
+from django.utils.safestring import mark_safe
+
+from jarbas.chamber_of_deputies.models import (
+ Reimbursement,
+ SocialMedia,
+)
+
+from jarbas.chamber_of_deputies.serializers import clean_cnpj_cpf
+from jarbas.dashboard.admin import list_filters, widgets
+from jarbas.dashboard.admin.paginators import CachedCountPaginator
+from jarbas.dashboard.admin.subquotas import Subquotas
+from jarbas.dashboard.admin.reimbursement_admin_model import ReimbursementAdmin
+from jarbas.public_admin.admin import PublicAdminModelAdmin
+
+
+ALL_FIELDS = sorted(Reimbursement._meta.fields, key=lambda f: f.verbose_name)
+CUSTOM_WIDGETS = ('receipt_url', 'subquota_description', 'suspicions')
+READONLY_FIELDS = (f.name for f in ALL_FIELDS if f.name not in CUSTOM_WIDGETS)
+
+
+class ChamberOfDeputiesReimbursementModelAdmin(ReimbursementAdmin):
+
+ list_display = (
+ 'short_document_id',
+ 'jarbas',
+ 'social_profile',
+ 'rosies_tweet',
+ 'receipt_link',
+ 'congressperson_name',
+ 'year',
+ 'subquota_translated',
+ 'supplier_info',
+ 'value',
+ 'suspicious'
+ )
+
+ search_fields = ('search_vector',)
+
+ list_filter = (
+ list_filters.SuspiciousListFilter,
+ list_filters.HasReceiptFilter,
+ list_filters.HasReimbursementNumberFilter,
+ list_filters.StateListFilter,
+ list_filters.YearListFilter,
+ list_filters.MonthListFilter,
+ list_filters.DocumentTypeListFilter,
+ list_filters.SubquotaListFilter,
+ )
+
+ fields = tuple(f.name for f in ALL_FIELDS)
+ readonly_fields = tuple(READONLY_FIELDS)
+ list_select_related = ('tweet',)
+ paginator = CachedCountPaginator
+
+ def jarbas(self, obj):
+ base_url = '/layers/#/documentId/{}/'
+ url = base_url.format(obj.document_id)
+ image = ''
+ return mark_safe('{}'.format(url, image))
+
+ jarbas.short_description = ''
+
+ def social_profile(self, obj):
+ social_media = SocialMedia.objects.filter(congressperson_id=obj.congressperson_id).first()
+ if not social_media:
+ return ''
+
+ tw_link = ''
+ tw_img = '/static/image/twitter-icon.png'
+ tw_profile = social_media.twitter_profile or social_media.secondary_twitter_profile
+ if tw_profile:
+ tw_link = ''.format(
+ tw_profile, tw_img
+ )
+
+ fb_link = ''
+ fb_img = '/static/image/facebook-icon.png'
+ if social_media.facebook_page:
+ fb_link = ''.format(
+ social_media.facebook_page, fb_img
+ )
+
+ return mark_safe(f'{tw_link} {fb_link}')
+
+ social_profile.short_description = 'Social'
+
+ def rosies_tweet(self, obj):
+ try:
+ return mark_safe('🤖'.format(obj.tweet.get_url()))
+ except Reimbursement.tweet.RelatedObjectDoesNotExist:
+ return ''
+
+ rosies_tweet.short_description = ''
+
+ def receipt_link(self, obj):
+ if not obj.receipt_url:
+ return ''
+ image = ''
+ return mark_safe('{}'.format(obj.receipt_url, image))
+
+ receipt_link.short_description = ''
+
+ def has_receipt_url(self, obj):
+ return obj.receipt_url is not None
+
+ has_receipt_url.short_description = 'recibo'
+ has_receipt_url.boolean = True
+
+ def value(self, obj):
+ return self._format_currency(obj.total_net_value)
+
+ value.short_description = 'valor'
+ value.admin_order_field = 'total_net_value'
+
+ def subquota_translated(self, obj):
+ return Subquotas.pt_br(obj.subquota_description)
+
+ def get_object(self, request, object_id, from_field=None):
+ obj = super().get_object(request, object_id, from_field)
+ if obj and not obj.receipt_fetched:
+ obj.get_receipt_url()
+ return obj
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ if db_field.name in CUSTOM_WIDGETS:
+ custom_widgets = dict(
+ subquota_description=widgets.SubquotaWidget,
+ receipt_url=widgets.ReceiptUrlWidget,
+ suspicions=widgets.SuspiciousWidget
+ )
+ kwargs['widget'] = custom_widgets.get(db_field.name)
+ return super().formfield_for_dbfield(db_field, **kwargs)
+
+ def get_search_results(self, request, queryset, search_term):
+ queryset, distinct = super(ChamberOfDeputiesReimbursementModelAdmin, self) \
+ .get_search_results(request, queryset, None)
+
+ if search_term:
+ # if a cnpj/cpf strip characters other than digits
+ search_term = clean_cnpj_cpf(search_term)
+ query = SearchQuery(search_term, config='portuguese')
+ rank = SearchRank(F('search_vector'), query)
+ queryset = queryset.annotate(rank=rank).filter(search_vector=query)
+
+ if not queryset.was_ordered():
+ queryset.order_by('-rank')
+
+ return queryset, distinct
+
+
+class ReimbursementSummaryModelAdmin(PublicAdminModelAdmin):
+ change_list_template = 'dashboard/reimbursement_summary_change_list.html'
+ list_filter = (
+ list_filters.SuspiciousListFilter,
+ list_filters.HasReimbursementNumberFilter,
+ list_filters.StateListFilter,
+ list_filters.YearListFilter,
+ list_filters.MonthListFilter,
+ list_filters.DocumentTypeListFilter,
+ )
+
+ def get_chart_grouping(self, request):
+ """Depending on the year selected on the sidebar filters, returns the
+ grouping criteria for the bottom bar chart:
+ * if user is seeing a page with no year filter, the chart shows
+ reimbursements grouped by year
+ * if the user is seeing a page filtered by a specific year, the chart
+ shows reimbursements grouped by month
+ """
+ if 'year' in request.GET:
+ return 'month'
+ return 'year'
+
+ @staticmethod
+ def serialize_summary_over_time(row, minimum_percentage='0.05', **kwargs):
+ low = kwargs.get('low') or Decimal('0')
+ high = kwargs.get('high') or Decimal('0')
+ chart_grouping = kwargs.get('chart_grouping')
+ chart_grouping_key = kwargs.get('chart_grouping_key')
+ minimum_percentage = Decimal(minimum_percentage)
+ total = row['total']
+
+ try:
+ percentage = (total - low) / (high - low)
+ except InvalidOperation:
+ percentage = Decimal('0')
+
+ ratio = Decimal('1') - minimum_percentage
+ corrected_percentage = minimum_percentage + (ratio * percentage)
+ bar_height = Decimal('100') * corrected_percentage
+
+ return {
+ 'label': chart_grouping,
+ 'chart_grouping': row[chart_grouping_key],
+ 'total': row['total'] or 0,
+ 'percent': bar_height
+ }
+
+ def get_cached_context(self, request, queryset):
+ url = request.build_absolute_uri()
+ hashed = md5(url.encode('utf-8')).hexdigest()
+ key = f'cached_reimbursement_summary_context_{hashed}'
+ context = cache.get(key)
+
+ if context is not None:
+ return context
+
+ metrics = {
+ 'total_reimbursements': Count('id'),
+ 'total_value': Sum('total_net_value'),
+ }
+ queryset = (
+ queryset
+ .values('subquota_description')
+ .annotate(**metrics)
+ .order_by('-total_value')
+ )
+
+ chart_grouping = self.get_chart_grouping(request)
+ if chart_grouping == 'year':
+ chart_grouping_key = 'year'
+ summary_over_time = (
+ queryset
+ .values('year')
+ .annotate(total=Sum('total_net_value'))
+ .order_by('year')
+ )
+ else:
+ chart_grouping_key = 'chart_grouping'
+ summary_over_time = (
+ queryset
+ .annotate(chart_grouping=Concat('year', 'month'))
+ .values('chart_grouping')
+ .annotate(total=Sum('total_net_value'))
+ .order_by('year', 'month')
+ )
+
+ summary_over_time = tuple(summary_over_time)
+ totals = tuple(row['total'] for row in summary_over_time)
+ over_time_args = {
+ 'chart_grouping': chart_grouping,
+ 'chart_grouping_key': chart_grouping_key,
+ 'low': min(totals, default=0),
+ 'high': max(totals, default=0)
+ }
+
+ context = {
+ 'year': request.GET.get('year'),
+ 'month': request.GET.get('month'),
+ 'chart_grouping': chart_grouping,
+ 'summary': tuple(queryset),
+ 'summary_total': dict(queryset.aggregate(**metrics)),
+ 'summary_over_time': tuple(
+ self.serialize_summary_over_time(row, **over_time_args)
+ for row in summary_over_time
+ )
+ }
+
+ cache.set(key, context, 60 * 60 * 6)
+ return context
+
+ def changelist_view(self, request, extra=None):
+ response = super().changelist_view(request, extra_context=extra)
+
+ try:
+ queryset = response.context_data['cl'].queryset
+ except (AttributeError, KeyError):
+ return response
+
+ context = self.get_cached_context(request, queryset)
+ response.context_data.update(context)
+ return response
diff --git a/jarbas/dashboard/admin/federal_senate.py b/jarbas/dashboard/admin/federal_senate.py
new file mode 100644
index 000000000..056925e74
--- /dev/null
+++ b/jarbas/dashboard/admin/federal_senate.py
@@ -0,0 +1,18 @@
+from jarbas.dashboard.admin.reimbursement_admin_model import ReimbursementAdmin
+
+
+class FederalSenateReimbursementModelAdmin(ReimbursementAdmin):
+ list_display = (
+ 'short_document_id',
+ 'congressperson_name',
+ 'year',
+ 'expense_type',
+ 'supplier_info',
+ 'value',
+ 'suspicious',
+ )
+
+ def value(self, obj):
+ return self._format_currency(obj.reimbursement_value)
+
+ value.short_description = 'valor'
diff --git a/jarbas/dashboard/admin/reimbursement_admin_model.py b/jarbas/dashboard/admin/reimbursement_admin_model.py
new file mode 100644
index 000000000..7de602c83
--- /dev/null
+++ b/jarbas/dashboard/admin/reimbursement_admin_model.py
@@ -0,0 +1,38 @@
+
+from brazilnum.cnpj import format_cnpj
+from brazilnum.cpf import format_cpf
+from django.utils.safestring import mark_safe
+
+from jarbas.public_admin.admin import PublicAdminModelAdmin
+
+
+class ReimbursementAdmin(PublicAdminModelAdmin):
+
+ def short_document_id(self, obj):
+ return obj.document_id
+
+ short_document_id.short_description = 'Reembolso'
+
+ def suspicious(self, obj):
+ return obj.suspicions is not None
+
+ suspicious.short_description = 'suspeito'
+ suspicious.boolean = True
+
+ def supplier_info(self, obj):
+ return mark_safe(f'{obj.supplier}
{self._format_document(obj)}')
+
+ supplier_info.short_description = 'Fornecedor'
+
+ def _format_document(self, obj):
+ if obj.cnpj_cpf:
+ if len(obj.cnpj_cpf) == 14:
+ return format_cnpj(obj.cnpj_cpf)
+
+ if len(obj.cnpj_cpf) == 11:
+ return format_cpf(obj.cnpj_cpf)
+
+ return obj.cnpj_cpf
+
+ def _format_currency(self, value):
+ return 'R$ {:.2f}'.format(value).replace('.', ',')
diff --git a/jarbas/dashboard/tests/test_dashboard_admin.py b/jarbas/dashboard/tests/test_dashboard_admin.py
index f6d944265..c47031515 100644
--- a/jarbas/dashboard/tests/test_dashboard_admin.py
+++ b/jarbas/dashboard/tests/test_dashboard_admin.py
@@ -4,9 +4,13 @@
from django.test import TestCase
from jarbas.chamber_of_deputies.models import Reimbursement
-from jarbas.dashboard.admin import ReimbursementModelAdmin
+from jarbas.dashboard.admin.chamber_of_deputies import (
+ ChamberOfDeputiesReimbursementModelAdmin
+)
from jarbas.dashboard.admin.list_filters import SubquotaListFilter
-from jarbas.dashboard.admin.widgets import ReceiptUrlWidget, SubquotaWidget, SuspiciousWidget
+from jarbas.dashboard.admin.widgets import (
+ ReceiptUrlWidget, SubquotaWidget, SuspiciousWidget
+)
Request = namedtuple('Request', ('method',))
@@ -17,7 +21,7 @@ class TestDashboardSite(TestCase):
def setUp(self):
self.requests = map(Request, ('GET', 'POST', 'PUT', 'PATCH', 'DELETE'))
- self.ma = ReimbursementModelAdmin(Reimbursement, 'dashboard')
+ self.ma = ChamberOfDeputiesReimbursementModelAdmin(Reimbursement, 'dashboard')
def test_has_add_permission(self):
permissions = map(self.ma.has_add_permission, self.requests)
diff --git a/jarbas/federal_senate/__init__.py b/jarbas/federal_senate/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/jarbas/federal_senate/app.py b/jarbas/federal_senate/app.py
new file mode 100644
index 000000000..7a1d40476
--- /dev/null
+++ b/jarbas/federal_senate/app.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class FederalSenateConfig(AppConfig):
+ name = 'jarbas.federal_senate'
+ verbose_name = "Senado Federal - Cota para ExercÃcio da Atividade Parlamentar"
diff --git a/jarbas/federal_senate/migrations/0001_initial.py b/jarbas/federal_senate/migrations/0001_initial.py
new file mode 100644
index 000000000..9f666d495
--- /dev/null
+++ b/jarbas/federal_senate/migrations/0001_initial.py
@@ -0,0 +1,43 @@
+# Generated by Django 2.0.8 on 2018-10-27 03:46
+
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Reimbursement',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('document_id', models.CharField(blank=True, max_length=140, null=True, verbose_name='Número do Reembolso')),
+ ('last_update', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Atualizado no Jarbas em')),
+ ('year', models.IntegerField(db_index=True, verbose_name='Ano')),
+ ('month', models.IntegerField(db_index=True, verbose_name='Mês')),
+ ('date', models.DateField(db_index=True, verbose_name='Data')),
+ ('congressperson_name', models.CharField(blank=True, db_index=True, max_length=140, null=True, verbose_name='Nome do Parlamentar')),
+ ('expense_type', models.CharField(blank=True, max_length=140, null=True, verbose_name='Tipo da Despesa')),
+ ('expense_details', models.CharField(blank=True, max_length=140, null=True, verbose_name='Descrição da Despesa')),
+ ('supplier', models.CharField(max_length=256, verbose_name='Fornecedor')),
+ ('cnpj_cpf', models.CharField(blank=True, db_index=True, max_length=14, null=True, verbose_name='CNPJ ou CPF')),
+ ('reimbursement_value', models.DecimalField(blank=True, decimal_places=3, max_digits=10, null=True, verbose_name='Valor do Reembolso')),
+ ('probability', models.DecimalField(blank=True, decimal_places=5, max_digits=6, null=True, verbose_name='Probabilidade')),
+ ('suspicions', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Suspeitas')),
+ ],
+ options={
+ 'verbose_name': 'reembolso',
+ 'verbose_name_plural': 'reembolsos',
+ 'ordering': ('-year', '-date'),
+ },
+ ),
+ migrations.AlterIndexTogether(
+ name='reimbursement',
+ index_together={('year', 'date', 'id')},
+ ),
+ ]
diff --git a/jarbas/federal_senate/migrations/__init__.py b/jarbas/federal_senate/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/jarbas/federal_senate/models.py b/jarbas/federal_senate/models.py
new file mode 100644
index 000000000..98efad31a
--- /dev/null
+++ b/jarbas/federal_senate/models.py
@@ -0,0 +1,32 @@
+from django.db import models
+from django.contrib.postgres.fields import JSONField
+
+
+class Reimbursement(models.Model):
+ document_id = models.CharField('Número do Reembolso', max_length=140, blank=True, null=True)
+ last_update = models.DateTimeField('Atualizado no Jarbas em', db_index=True, auto_now=True)
+
+ year = models.IntegerField('Ano', db_index=True)
+ month = models.IntegerField('Mês', db_index=True)
+ date = models.DateField('Data', db_index=True)
+
+ congressperson_name = models.CharField('Nome do Parlamentar', max_length=140, db_index=True, blank=True, null=True)
+
+ expense_type = models.CharField('Tipo da Despesa', max_length=140, blank=True, null=True)
+ expense_details = models.CharField('Descrição da Despesa', max_length=140, blank=True, null=True)
+
+ supplier = models.CharField('Fornecedor', max_length=256)
+ cnpj_cpf = models.CharField('CNPJ ou CPF', max_length=14, db_index=True, blank=True, null=True)
+
+ reimbursement_value = models.DecimalField(
+ 'Valor do Reembolso', max_digits=10, decimal_places=3, blank=True, null=True
+ )
+
+ probability = models.DecimalField('Probabilidade', max_digits=6, decimal_places=5, blank=True, null=True)
+ suspicions = JSONField('Suspeitas', blank=True, null=True)
+
+ class Meta:
+ ordering = ('-year', '-date')
+ verbose_name = 'reembolso'
+ verbose_name_plural = 'reembolsos'
+ index_together = [['year', 'date', 'id']]
diff --git a/jarbas/federal_senate/tests/__init__.py b/jarbas/federal_senate/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/jarbas/federal_senate/tests/test_reimbursement_model.py b/jarbas/federal_senate/tests/test_reimbursement_model.py
new file mode 100644
index 000000000..fa55d30c8
--- /dev/null
+++ b/jarbas/federal_senate/tests/test_reimbursement_model.py
@@ -0,0 +1,52 @@
+from django.test import TestCase
+
+from jarbas.federal_senate.models import Reimbursement
+
+
+class TestReimbursement(TestCase):
+
+ def setUp(self):
+ self.data = dict(
+ document_id='214',
+ year=2013,
+ month=5,
+ date='2013-10-05',
+ congressperson_name='MOZARILDO CAVALCANTI',
+ expense_type='Publicity of parliamentary activity',
+ expense_details='Despesas com divulgação da atividade parlamentar do Senador Mozarildo Cavalcanti',
+ supplier='Editora Zenite Ltda.',
+ cnpj_cpf='08509060000146',
+ reimbursement_value=300,
+ probability=0.5,
+ suspicions={'invalid_cnpj_cpf': {'is_suspect': True, 'probability': 1.0}},
+ )
+
+
+class TestCreate(TestReimbursement):
+
+ def test_create(self):
+ self.assertEqual(0, Reimbursement.objects.count())
+ Reimbursement.objects.create(**self.data)
+ self.assertEqual(1, Reimbursement.objects.count())
+
+ def test_last_update(self):
+ reimbursement = Reimbursement.objects.create(**self.data)
+ created_at = reimbursement.last_update
+ reimbursement.year = 1971
+ reimbursement.save()
+ self.assertGreater(reimbursement.last_update, created_at)
+
+ def test_optional_fields(self):
+ optional = (
+ 'document_id',
+ 'congressperson_name',
+ 'expense_type',
+ 'expense_details',
+ 'cnpj_cpf',
+ 'reimbursement_value',
+ 'probability',
+ 'suspicions'
+ )
+ new_data = {k: v for k, v in self.data.items() if k not in optional}
+ Reimbursement.objects.create(**new_data)
+ self.assertEqual(1, Reimbursement.objects.count())
diff --git a/jarbas/federal_senate/views.py b/jarbas/federal_senate/views.py
new file mode 100644
index 000000000..91ea44a21
--- /dev/null
+++ b/jarbas/federal_senate/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/jarbas/public_admin/sites.py b/jarbas/public_admin/sites.py
index c4d5b7da2..e71fe40bb 100644
--- a/jarbas/public_admin/sites.py
+++ b/jarbas/public_admin/sites.py
@@ -8,10 +8,13 @@
class DummyUser(AnonymousUser):
def has_module_perms(self, app_label):
- return app_label == 'chamber_of_deputies'
+ return app_label in ('chamber_of_deputies', 'federal_senate')
def has_perm(self, permission, obj=None):
- return permission == 'chamber_of_deputies.change_reimbursement'
+ return permission in (
+ 'chamber_of_deputies.change_reimbursement',
+ 'federal_senate.change_reimbursement'
+ )
class PublicAdminSite(AdminSite):
diff --git a/jarbas/settings.py b/jarbas/settings.py
index 29dc29166..f59d1f0b9 100644
--- a/jarbas/settings.py
+++ b/jarbas/settings.py
@@ -48,6 +48,7 @@
'rest_framework',
'jarbas.core.app.CoreConfig',
'jarbas.chamber_of_deputies.app.ChamberOfDeputiesConfig',
+ 'jarbas.federal_senate.app.FederalSenateConfig',
'jarbas.layers',
'jarbas.dashboard',
'django.contrib.admin',