diff --git a/finances/admin.py b/finances/admin.py index effd61f1080b9a1c58cf789ad3aa77ad9c07f87a..c887dee025b18e2249c89740f90d08ad10be7f57 100644 --- a/finances/admin.py +++ b/finances/admin.py @@ -4,7 +4,9 @@ __license__ = "AGPL v3" from django.contrib import admin -from .models import WorkLog +from .models import Subsidy, WorkLog +admin.site.register(Subsidy) + admin.site.register(WorkLog) diff --git a/finances/constants.py b/finances/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..e5f21aad2669aa93f5614a6a97cd2d04fab2ddb9 --- /dev/null +++ b/finances/constants.py @@ -0,0 +1,38 @@ +__copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +import datetime + + +SUBSIDY_TYPE_GRANT = 'grant' +SUBSIDY_TYPE_PARTNERAGREEMENT = 'partneragreement' +SUBSIDY_TYPE_COLLABORATION = 'collaborationagreement' + +SUBSIDY_TYPES = ( + (SUBSIDY_TYPE_GRANT, 'Grant'), + (SUBSIDY_TYPE_PARTNERAGREEMENT, 'Partner Agreement'), + (SUBSIDY_TYPE_COLLABORATION, 'Collaboration Agreement'), +) + + +SUBSIDY_PROMISED = 'promised' +SUBSIDY_INVOICED = 'invoiced' +SUBSIDY_RECEIVED = 'received' + +SUBSIDY_STATUS = ( + (SUBSIDY_PROMISED, 'promised'), + (SUBSIDY_INVOICED, 'invoiced'), + (SUBSIDY_RECEIVED, 'received'), +) + + +SUBSIDY_DURATION = ( + (datetime.timedelta(days=365), '1 year'), + (datetime.timedelta(days=730), '2 years'), + (datetime.timedelta(days=1095), '3 years'), + (datetime.timedelta(days=1460), '4 years'), + (datetime.timedelta(days=1825), '5 years'), + (datetime.timedelta(days=3650), '10 years'), + (datetime.timedelta(days=36500), 'Indefinite (100 years)'), +) diff --git a/finances/migrations/0002_subsidy.py b/finances/migrations/0002_subsidy.py new file mode 100644 index 0000000000000000000000000000000000000000..a66d36940496ab56e3d7efdb796394c9bda3afae --- /dev/null +++ b/finances/migrations/0002_subsidy.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-10-06 21:41 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0002_populate_from_partners_org'), + ('finances', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Subsidy', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subsidy_type', models.CharField(choices=[('grant', 'Grant'), ('partneragreement', 'Partner Agreement'), ('collaborationagreement', 'Collaboration Agreement')], max_length=256)), + ('description', models.CharField(max_length=256)), + ('amount', models.PositiveSmallIntegerField()), + ('status', models.CharField(choices=[('promised', 'promised'), ('invoiced', 'invoiced'), ('received', 'received')], max_length=32)), + ('date', models.DateField()), + ('duration', models.DurationField(blank=True, choices=[(datetime.timedelta(365), '1 year'), (datetime.timedelta(730), '2 years'), (datetime.timedelta(1095), '3 years'), (datetime.timedelta(1460), '4 years'), (datetime.timedelta(1825), '5 years'), (datetime.timedelta(3650), '10 years'), (datetime.timedelta(36500), 'Indefinite (100 years)')], null=True)), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization')), + ], + options={ + 'verbose_name_plural': 'subsidies', + }, + ), + ] diff --git a/finances/models.py b/finances/models.py index 1343616c535be0cc03bd555f255164864a72a15b..415efa04134e4e4dd7fc713282d4e6a8ff32fe34 100644 --- a/finances/models.py +++ b/finances/models.py @@ -8,9 +8,37 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models from django.utils import timezone +from .constants import SUBSIDY_TYPES, SUBSIDY_STATUS, SUBSIDY_DURATION from .utils import id_to_slug +class Subsidy(models.Model): + """ + A subsidy given to SciPost by an Organization. + Any fund given to SciPost, in any form, must be associated + to a corresponding Subsidy instance. + + This can for example be: + - a Partners agreement + - an incidental grant + - a development grant for a specific purpose + - a Collaboration Agreement + """ + organization = models.ForeignKey('organizations.Organization', on_delete=models.CASCADE) + subsidy_type = models.CharField(max_length=256, choices=SUBSIDY_TYPES) + description = models.CharField(max_length=256) + amount = models.PositiveSmallIntegerField() + status = models.CharField(max_length=32, choices=SUBSIDY_STATUS) + date = models.DateField() + duration = models.DurationField(choices=SUBSIDY_DURATION, blank=True, null=True) + + class Meta: + verbose_name_plural = 'subsidies' + + def __str__(self): + return '%s: %s, %s' % (self.date, self.organization, self.description) + + class WorkLog(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL) comments = models.TextField(blank=True) diff --git a/finances/templates/finances/subsidy_confirm_delete.html b/finances/templates/finances/subsidy_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..d50e6c7e5bee3de6fd93eae86010e4beef387ed3 --- /dev/null +++ b/finances/templates/finances/subsidy_confirm_delete.html @@ -0,0 +1,25 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Delete Subsidy{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Delete Subsidy</h1> + {{ object }} + </div> +</div> +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + <h3 class="mb-2">Are you sure you want to delete this Subsidy?</h3> + <input type="submit" class="btn btn-danger" value="Yes, delete it" /> + </form> + </ul> + </div> +</div> + +{% endblock content %} diff --git a/finances/templates/finances/subsidy_form.html b/finances/templates/finances/subsidy_form.html new file mode 100644 index 0000000000000000000000000000000000000000..5ed78daa76f3d42652582c168c0de562f89520c7 --- /dev/null +++ b/finances/templates/finances/subsidy_form.html @@ -0,0 +1,21 @@ +{% extends 'profiles/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">{% if form.instance.id %}Update {{ form.instance }}{% else %}Add new Subsidy{% endif %}</span> +{% endblock %} + +{% block pagetitle %}: Subsidies{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <form action="" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" value="Submit" class="btn btn-primary"> + </div> +</div> +{% endblock content %} diff --git a/finances/templates/finances/subsidy_list.html b/finances/templates/finances/subsidy_list.html new file mode 100644 index 0000000000000000000000000000000000000000..1d5593cea51b209beb107dbc59614cdd41ea63f1 --- /dev/null +++ b/finances/templates/finances/subsidy_list.html @@ -0,0 +1,20 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Subsidies{% endblock pagetitle %} + + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h3>Subsidies</h3> + <ul> + {% for subsidy in object_list %} + <li>{{ subsidy }}</li> + {% empty %} + <li>No Subsidy found</li> + {% endfor %} + </div> +</div> + +{% endblock content %} diff --git a/finances/urls.py b/finances/urls.py index 7299cbd1ee55d9619e8a4abd7df0b93cf70769bb..865cd0f7acac62886d08f6c8985230138083ccc3 100644 --- a/finances/urls.py +++ b/finances/urls.py @@ -8,6 +8,29 @@ from . import views urlpatterns = [ url(r'^$', views.timesheets, name='finance'), + + url( + r'^subsidies/$', + views.SubsidyListView.as_view(), + name='subsidies' + ), + url( + r'^subsidies/add/$', + views.SubsidyCreateView.as_view(), + name='subsidy_create' + ), + url( + r'^subsidies/(?P<pk>[0-9]+)/update/$', + views.SubsidyUpdateView.as_view(), + name='subsidy_update' + ), + url( + r'^subsidies/(?P<pk>[0-9]+)/delete/$', + views.SubsidyDeleteView.as_view(), + name='subsidy_delete' + ), + + url(r'^timesheets$', views.timesheets, name='timesheets'), url(r'^logs/(?P<slug>\d+)/delete$', views.LogDeleteView.as_view(), name='log_delete'), ] diff --git a/finances/views.py b/finances/views.py index 34123e7e256a0244aea236a9657143e46fc6f965..11071a1000e20fc9835b7e761ce6399cea8eeca0 100644 --- a/finances/views.py +++ b/finances/views.py @@ -5,14 +5,54 @@ __license__ = "AGPL v3" from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.urlresolvers import reverse_lazy from django.http import Http404 from django.shortcuts import render -from django.views.generic.edit import DeleteView +from django.views.generic.detail import DetailView +from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.views.generic.list import ListView from .forms import LogsMonthlyActiveFilter -from .models import WorkLog +from .models import Subsidy, WorkLog from .utils import slug_to_id +from scipost.mixins import PermissionsMixin + + +class SubsidyCreateView(PermissionsMixin, CreateView): + """ + Create a new Subsidy. + """ + permission_required = 'scipost.can_manage_subsidies' + model = Subsidy + fields = '__all__' + template_name = 'finances/subsidy_create.html' + success_url = reverse_lazy('finances:subsidies') + + +class SubsidyUpdateView(PermissionsMixin, UpdateView): + """ + Update a Subsidy. + """ + permission_required = 'scipost.can_manage_subsidies' + model = Subsidy + fields = '__all__' + template_name = 'finances/subsidy_update.html' + success_url = reverse_lazy('finances:subsidies') + + +class SubsidyDeleteView(PermissionsMixin, DeleteView): + """ + Delete a Subsidy. + """ + permission_required = 'scipost.can_manage_subsidies' + model = Subsidy + success_url = reverse_lazy('finances:subsidies') + + +class SubsidyListView(ListView): + model = Subsidy + @permission_required('scipost.can_view_timesheets', raise_exception=True) def timesheets(request): diff --git a/organizations/templates/organizations/_organization_card.html b/organizations/templates/organizations/_organization_card.html index a52c04bd7126231371a09412797404b9ae5586d1..0f42473c9e3fa68e1514f9092741287cc3ce7d7f 100644 --- a/organizations/templates/organizations/_organization_card.html +++ b/organizations/templates/organizations/_organization_card.html @@ -18,10 +18,10 @@ <a class="nav-link" id="authors-{{ org.id }}-tab" data-toggle="tab" href="#authors-{{ org.id }}" role="tab" aria-controls="authors-{{ org.id }}" aria-selected="true">Associated Authors</a> </li> <li class="nav-item"> - <a class="nav-link" id="funders-{{ org.id }}-tab" data-toggle="tab" href="#funders-{{ org.id }}" role="tab" aria-controls="funders-{{ org.id }}" aria-selected="true">FundRef instances</a> + <a class="nav-link" id="funders-{{ org.id }}-tab" data-toggle="tab" href="#funders-{{ org.id }}" role="tab" aria-controls="funders-{{ org.id }}" aria-selected="true">Funder Registry instances</a> </li> <li class="nav-item"> - <a class="nav-link" id="partnership-{{ org.id }}-tab" data-toggle="tab" href="#partnership-{{ org.id }}" role="tab" aria-controls="partnership-{{ org.id }}" aria-selected="true">Partnership history</a> + <a class="nav-link" id="support-{{ org.id }}-tab" data-toggle="tab" href="#support-{{ org.id }}" role="tab" aria-controls="support-{{ org.id }}" aria-selected="true">Support history</a> </li> {% if perms.scipost.can_manage_organizations %} <li class="nav-item"> @@ -82,25 +82,25 @@ </div> <div class="tab-pane pt-4" id="funders-{{ org.id }}" role="tabpanel" aria-labelledby="funders-{{ org.id }}-tab"> - <h3>FundRef instances associated to this Organization:</h3> + <h3>Funder Registry instances associated to this Organization:</h3> <ul> {% for funder in org.funder_set.all %} <li>{{ funder }}</li> {% empty %} - <li>No FundRef instance found<br/><br/> - <strong class="text-danger">Without a FundRef instance, we cannot record funding acknowledgements to this Organization at Crossref.</strong> + <li>No Funder Registry instance found<br/><br/> + <strong class="text-danger">Without a Funder Registry instance, we cannot record funding acknowledgements to this Organization with Crossref.</strong> <p>Are you a representative of this Organization? Please:</p> <ol> - <li>Make sure your Organization is included in Crossref's FundRef database</li> - <li>After inclusion, <a href="mailto:admin@scipost.org?subject=Inclusion of {{ organization }} {% if organization.acronym %}({{ organization.acronym }}){% endif %} in FundRef">contact our administration</a> with this information so that we can update our records.</li> + <li>Make sure your Organization is included in <a href="https://www.crossref.org/services/funder-registry/" target="_blank">Crossref's Funder Registry</a></li> + <li>After inclusion, <a href="mailto:admin@scipost.org?subject=Inclusion of {{ organization }} {% if organization.acronym %}({{ organization.acronym }}){% endif %} in the Funder Registry">contact our administration</a> with this information so that we can update our records.</li> </ol> </li> {% endfor %} </ul> </div> - <div class="tab-pane pt-4" id="partnership-{{ org.id }}" role="tabpanel" aria-labelledby="partnership-{{ org.id }}-tab"> - <h3>Partnership history:</h3> + <div class="tab-pane pt-4" id="support-{{ org.id }}" role="tabpanel" aria-labelledby="support-{{ org.id }}-tab"> + <h3>Supporting Partner Agreements history:</h3> {% with agreement=org.partner.get_latest_active_agreement %} {% if agreement %} <p>This organization is currently a SciPost Supporting Partner.</p> diff --git a/profiles/migrations/0011_auto_20181006_2341.py b/profiles/migrations/0011_auto_20181006_2341.py new file mode 100644 index 0000000000000000000000000000000000000000..af1221f59d1a8680dd162060137682a46292c50e --- /dev/null +++ b/profiles/migrations/0011_auto_20181006_2341.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-10-06 21:41 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0010_auto_20181002_1114'), + ] + + operations = [ + migrations.AlterModelOptions( + name='profileemail', + options={'ordering': ['-primary', '-still_valid', 'email']}, + ), + migrations.RemoveField( + model_name='profile', + name='email', + ), + ] diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 928cd548f77e93ba1c90057be7a816dc99726922..f481674d151cdc5673b6d120352ff9a147b9a850 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -291,6 +291,10 @@ class Command(BaseCommand): content_type=content_type) # Financial administration + can_manage_subsidies, created = Permission.objects.get_or_create( + codename='can_manage_subsidies', + name='Can manage subsidies', + content_type=content_type) can_view_timesheets, created = Permission.objects.get_or_create( codename='can_view_timesheets', name='Can view timesheets',