wip/71911-invoicing-ui (#71911) #3
|
@ -0,0 +1,54 @@
|
||||||
|
# lingo - payment and billing system
|
||||||
|
# Copyright (C) 2022 Entr'ouvert
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from lingo.invoicing.models import Campaign
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Campaign
|
||||||
|
fields = ['date_start', 'date_end', 'date_issue']
|
||||||
|
widgets = {
|
||||||
|
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||||
|
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||||
|
'date_issue': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
if 'date_start' in cleaned_data and 'date_end' in cleaned_data:
|
||||||
|
overlapping_qs = Campaign.objects.extra(
|
||||||
|
where=["(date_start, date_end) OVERLAPS (%s, %s)"],
|
||||||
|
params=[cleaned_data['date_start'], cleaned_data['date_end']],
|
||||||
|
)
|
||||||
|
if self.instance.pk:
|
||||||
|
overlapping_qs = overlapping_qs.exclude(pk=self.instance.pk)
|
||||||
|
if overlapping_qs.exists():
|
||||||
|
self.add_error(None, _('Another campaign overlapping this period already exists.'))
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class PoolForm(forms.Form):
|
||||||
|
draft = forms.BooleanField(label=_('Run a simulation'), initial=True, required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.campaign = kwargs.pop('campaign')
|
||||||
|
super().__init__(*args, **kwargs)
|
|
@ -19,6 +19,7 @@ import copy
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.formats import date_format
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
@ -92,6 +93,12 @@ class Campaign(models.Model):
|
||||||
date_end = models.DateField(_('End date'))
|
date_end = models.DateField(_('End date'))
|
||||||
date_issue = models.DateField(_('Issue date'))
|
date_issue = models.DateField(_('Issue date'))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _('From %(start)s to %(end)s') % {
|
||||||
|
'start': date_format(self.date_start, 'd/m/Y'),
|
||||||
|
'end': date_format(self.date_end, 'd/m/Y'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Pool(models.Model):
|
class Pool(models.Model):
|
||||||
campaign = models.ForeignKey(Campaign, on_delete=models.PROTECT)
|
campaign = models.ForeignKey(Campaign, on_delete=models.PROTECT)
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
{% extends "lingo/invoicing/manager_campaign_list.html" %}
|
||||||
lguerin marked this conversation as resolved
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
{{ block.super }}
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-detail' object.pk %}">{{ object }}</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block appbar %}
|
||||||
|
<h2>{{ object }}</h2>
|
||||||
|
{% if not has_real_pool %}
|
||||||
|
<span class="actions">
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-delete' pk=object.pk %}" rel="popup">{% trans "Delete" %}</a>
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-edit' pk=object.pk %}" rel="popup">{% trans "Edit" %}</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="section">
|
||||||
|
<div class="pk-tabs">
|
||||||
|
<div class="pk-tabs--tab-list" role="tablist">
|
||||||
|
<button aria-controls="panel-settings" aria-selected="true" id="tab-settings" role="tab" tabindex="0">{% trans "Settings" %}</button>
|
||||||
|
<button aria-controls="panel-pools" aria-selected="false" id="tab-pools" role="tab" tabindex="-1">{% trans "Pools" %}</button>
|
||||||
|
</div>
|
||||||
|
<div class="pk-tabs--container">
|
||||||
|
|
||||||
|
<div aria-labelledby="tab-settings" id="panel-settings" role="tabpanel" tabindex="0">
|
||||||
|
<ul>
|
||||||
|
<li>{% trans "Start date:" %} {{ object.date_start|date:'d/m/Y' }}</li>
|
||||||
|
<li>{% trans "End date:" %} {{ object.date_end|date:'d/m/Y' }}</li>
|
||||||
|
<li>{% trans "Issue date:" %} {{ object.date_issue|date:'d/m/Y' }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div aria-labelledby="tab-pools" hidden="" id="panel-pools" role="tabpanel" tabindex="0">
|
||||||
|
<ul class="objects-list single-links">
|
||||||
|
{% for pool in pools %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-pool-detail' pk=object.pk pool_pk=pool.pk %}">
|
||||||
|
{{ pool.created_at|date:'DATETIME_FORMAT' }}
|
||||||
|
{% if pool.draft %}<span class="badge">{% trans "draft" %}</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% if not has_real_pool %}
|
||||||
|
<div class="panel--buttons">
|
||||||
|
<a class="pk-button" rel="popup" href="{% url 'lingo-manager-invoicing-pool-add' pk=object.pk %}">{% trans 'Start a pool' %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends "lingo/invoicing/manager_campaign_list.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if form.instance.pk %}
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-detail' object.pk %}">{{ object }}</a>
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-edit' object.pk %}">{% trans "Edit" %}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-add' %}">{% trans "New campaign" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block appbar %}
|
||||||
|
{% if object.pk %}
|
||||||
|
<h2>{% trans "Edit campaign" %}</h2>
|
||||||
|
{% else %}
|
||||||
|
<h2>{% trans "New campaign" %}</h2>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="submit-button">{% trans "Save" %}</button>
|
||||||
|
{% if object.pk %}
|
||||||
|
<a class="cancel" href="{% url 'lingo-manager-invoicing-campaign-detail' object.pk %}">{% trans 'Cancel' %}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="cancel" href="{% url 'lingo-manager-invoicing-campaign-list' %}">{% trans 'Cancel' %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,42 @@
|
||||||
|
{% extends "lingo/invoicing/manager_home.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
{{ block.super }}
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-list' %}">{% trans "Campaigns" %}</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block appbar %}
|
||||||
|
<h2>{% trans "Campaigns" %}</h2>
|
||||||
|
<span class="actions">
|
||||||
|
<a rel="popup" href="{% url 'lingo-manager-invoicing-campaign-add' %}">{% trans 'New campaign' %}</a>
|
||||||
|
</span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pk-information">
|
||||||
|
<p>{% trans "Manage here invoicing campaigns." %}</p>
|
||||||
|
</div>
|
||||||
|
{% if object_list %}
|
||||||
|
<div>
|
||||||
|
<h3>{% trans "Campaigns" %}</h3>
|
||||||
|
<ul class="objects-list single-links">
|
||||||
|
{% for object in object_list %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-detail' pk=object.pk %}">
|
||||||
|
{{ object }}
|
||||||
|
<span class="extra-info"> [{% trans "issue date:" %} {{ object.date_issue|date:'d/m/Y' }}]</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="big-msg-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This site doesn't have any campaign yet. Click on the "New campaign" button in the top
|
||||||
|
right of the page to add a first one.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -17,6 +17,10 @@
|
||||||
{% trans "Regies" %}
|
{% trans "Regies" %}
|
||||||
<p>{% trans "Invoicing regies." %}</p>
|
<p>{% trans "Invoicing regies." %}</p>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-campaign-list' %}">
|
||||||
|
{% trans "Campaigns" %}
|
||||||
|
<p>{% trans "Manage invoicing campaigns." %}</p>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
{% extends "lingo/invoicing/manager_campaign_detail.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
{{ block.super }}
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-pool-detail' pk=object.pk pool_pk=pool.pk %}">{% trans "Pool" %}</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block appbar %}
|
||||||
|
<h2>{{ pool.created_at|date:"DATETIME_FORMAT" }}</h2>
|
||||||
|
{% if pool.draft %}
|
||||||
|
<span class="actions">
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-pool-delete' pk=object.pk pool_pk=pool.pk %}" rel="popup">{% trans "Delete" %}</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<table class="main">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "PK" %}</th>
|
||||||
|
<th>{% trans "Invoice PK" %}</th>
|
||||||
|
<th>{% trans "Label" %}</th>
|
||||||
|
<th>{% trans "Event" %}</th>
|
||||||
|
<th>{% trans "Quantity" %}</th>
|
||||||
|
<th>{% trans "Unit amount" %}</th>
|
||||||
|
<th>{% trans "Total amount" %}</th>
|
||||||
|
<th>{% trans "User" %}</th>
|
||||||
|
<th>{% trans "Payer" %}</th>
|
||||||
|
<th>{% trans "Status" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for line in lines %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ line.pk }}</td>
|
||||||
|
<td>{{ line.invoice_id|default:'' }}</td>
|
||||||
|
<td>{{ line.label }}</td>
|
||||||
|
<td>{{ line.event.slug }}</td>
|
||||||
|
<td>{{ line.quantity }}</td>
|
||||||
|
<td>{{ line.unit_amount }}</td>
|
||||||
|
<td>{{ line.total_amount }}</td>
|
||||||
|
<td>{{ line.user_external_id }}</td>
|
||||||
|
<td>{{ line.adult_external_id }}</td>
|
||||||
|
<td>{{ line.get_status_display }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "lingo/invoicing/manager_campaign_detail.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
{{ block.super }}
|
||||||
|
<a href="{% url 'lingo-manager-invoicing-campaign-add' %}">{% trans "Start a pool" %}</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block appbar %}
|
||||||
|
<h2>{% trans "New pool" %}</h2>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="submit-button">{% trans "Run" %}</button>
|
||||||
|
<a class="cancel" href="{% url 'lingo-manager-invoicing-campaign-detail' object.pk %}">{% trans 'Cancel' %}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -43,4 +43,40 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
path('regies/import/', views.regies_import, name='lingo-manager-invoicing-regie-import'),
|
path('regies/import/', views.regies_import, name='lingo-manager-invoicing-regie-import'),
|
||||||
path('regies/export/', views.regies_export, name='lingo-manager-invoicing-regie-export'),
|
path('regies/export/', views.regies_export, name='lingo-manager-invoicing-regie-export'),
|
||||||
|
path('campaigns/', views.campaign_list, name='lingo-manager-invoicing-campaign-list'),
|
||||||
|
path(
|
||||||
|
'campaign/add/',
|
||||||
|
views.campaign_add,
|
||||||
|
name='lingo-manager-invoicing-campaign-add',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'campaign/<int:pk>/',
|
||||||
|
views.campaign_detail,
|
||||||
|
name='lingo-manager-invoicing-campaign-detail',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'campaign/<int:pk>/edit/',
|
||||||
|
views.campaign_edit,
|
||||||
|
name='lingo-manager-invoicing-campaign-edit',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'campaign/<int:pk>/delete/',
|
||||||
|
views.campaign_delete,
|
||||||
|
name='lingo-manager-invoicing-campaign-delete',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'campaign/<int:pk>/pool/add/',
|
||||||
|
views.pool_add,
|
||||||
|
name='lingo-manager-invoicing-pool-add',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'campaign/<int:pk>/pool/<int:pool_pk>/',
|
||||||
|
views.pool_detail,
|
||||||
|
name='lingo-manager-invoicing-pool-detail',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'campaign/<int:pk>/pool/<int:pool_pk>/delete/',
|
||||||
|
views.pool_delete,
|
||||||
|
name='lingo-manager-invoicing-pool-delete',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,6 +21,7 @@ import json
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.translation import ungettext
|
from django.utils.translation import ungettext
|
||||||
|
@ -35,7 +36,17 @@ from django.views.generic import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from lingo.agendas.models import Agenda
|
from lingo.agendas.models import Agenda
|
||||||
from lingo.invoicing.models import Regie, RegieImportError
|
from lingo.invoicing.forms import CampaignForm, PoolForm
|
||||||
|
from lingo.invoicing.models import (
|
||||||
|
Campaign,
|
||||||
|
DraftInvoice,
|
||||||
|
DraftInvoiceLine,
|
||||||
|
InvoiceLine,
|
||||||
|
Pool,
|
||||||
|
Regie,
|
||||||
|
RegieImportError,
|
||||||
|
)
|
||||||
|
from lingo.invoicing.utils import generate_invoices
|
||||||
from lingo.pricing.forms import ImportForm
|
from lingo.pricing.forms import ImportForm
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,3 +186,150 @@ class RegiesImportView(FormView):
|
||||||
|
|
||||||
|
|
||||||
regies_import = RegiesImportView.as_view()
|
regies_import = RegiesImportView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignListView(ListView):
|
||||||
|
template_name = 'lingo/invoicing/manager_campaign_list.html'
|
||||||
|
model = Campaign
|
||||||
|
|
||||||
|
|
||||||
|
campaign_list = CampaignListView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignAddView(CreateView):
|
||||||
|
template_name = 'lingo/invoicing/manager_campaign_form.html'
|
||||||
|
model = Campaign
|
||||||
|
form_class = CampaignForm
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('lingo-manager-invoicing-campaign-detail', args=[self.object.pk])
|
||||||
|
|
||||||
|
|
||||||
|
campaign_add = CampaignAddView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignDetailView(DetailView):
|
||||||
|
template_name = 'lingo/invoicing/manager_campaign_detail.html'
|
||||||
|
model = Campaign
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs['pools'] = self.object.pool_set.order_by('created_at')
|
||||||
|
kwargs['has_real_pool'] = any(not p.draft for p in kwargs['pools'])
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
campaign_detail = CampaignDetailView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignEditView(UpdateView):
|
||||||
|
template_name = 'lingo/invoicing/manager_campaign_form.html'
|
||||||
|
model = Campaign
|
||||||
|
form_class = CampaignForm
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().exclude(pool__draft=False)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('lingo-manager-invoicing-campaign-detail', args=[self.object.pk])
|
||||||
|
|
||||||
|
|
||||||
|
campaign_edit = CampaignEditView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignDeleteView(DeleteView):
|
||||||
|
template_name = 'lingo/manager_confirm_delete.html'
|
||||||
|
model = Campaign
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().exclude(pool__draft=False)
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
DraftInvoiceLine.objects.filter(pool__campaign=self.object).delete()
|
||||||
|
DraftInvoice.objects.filter(pool__campaign=self.object).delete()
|
||||||
|
Pool.objects.filter(campaign=self.object).delete()
|
||||||
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('lingo-manager-invoicing-campaign-list')
|
||||||
|
|
||||||
|
|
||||||
|
campaign_delete = CampaignDeleteView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class PoolDetailView(DetailView):
|
||||||
|
template_name = 'lingo/invoicing/manager_pool_detail.html'
|
||||||
|
model = Pool
|
||||||
|
pk_url_kwarg = 'pool_pk'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.campaign = get_object_or_404(Campaign, pk=kwargs['pk'])
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.campaign.pool_set.all()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs['object'] = self.campaign
|
||||||
|
kwargs['pool'] = self.object
|
||||||
|
line_model = InvoiceLine
|
||||||
|
if self.object.draft:
|
||||||
|
line_model = DraftInvoiceLine
|
||||||
|
kwargs['lines'] = line_model.objects.filter(pool=self.object)
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
pool_detail = PoolDetailView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class PoolAddView(FormView):
|
||||||
|
template_name = 'lingo/invoicing/manager_pool_form.html'
|
||||||
|
form_class = PoolForm
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.object = get_object_or_404(Campaign.objects.exclude(pool__draft=False), pk=kwargs['pk'])
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['campaign'] = self.object
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs['object'] = self.object
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
generate_invoices(campaign=self.object, draft=form.cleaned_data['draft'])
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return '%s#open:pools' % reverse('lingo-manager-invoicing-campaign-detail', args=[self.object.pk])
|
||||||
|
|
||||||
|
|
||||||
|
pool_add = PoolAddView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class PoolDeleteView(DeleteView):
|
||||||
|
template_name = 'lingo/manager_confirm_delete.html'
|
||||||
|
model = Pool
|
||||||
|
pk_url_kwarg = 'pool_pk'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.campaign = get_object_or_404(Campaign.objects.exclude(pool__draft=False), pk=kwargs['pk'])
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.campaign.pool_set.filter(draft=True)
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
DraftInvoiceLine.objects.filter(pool=self.object).delete()
|
||||||
|
DraftInvoice.objects.filter(pool=self.object).delete()
|
||||||
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return '%s#open:pools' % reverse('lingo-manager-invoicing-campaign-detail', args=[self.campaign.pk])
|
||||||
|
|
||||||
|
|
||||||
|
pool_delete = PoolDeleteView.as_view()
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from tests.utils import login
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_manager_home_show_invoicing(app, admin_user):
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/')
|
||||||
|
anchor = resp.pyquery('div#appbar span.actions a[href="%s"]' % reverse('lingo-manager-invoicing-home'))
|
||||||
|
assert anchor.text() == 'Invoicing'
|
||||||
|
|
||||||
|
|
||||||
|
def test_manager_invoicing_home(app, admin_user):
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get(reverse('lingo-manager-invoicing-home'))
|
||||||
|
h2 = resp.pyquery('div#appbar h2')
|
||||||
|
assert h2.text() == 'Invoicing'
|
||||||
|
anchor = resp.pyquery(
|
||||||
|
'div#lingo-manager-main div a[href="%s"]' % reverse('lingo-manager-invoicing-regie-list')
|
||||||
|
)
|
||||||
|
assert anchor.text().startswith('Regies')
|
||||||
|
anchor = resp.pyquery(
|
||||||
|
'div#lingo-manager-main div a[href="%s"]' % reverse('lingo-manager-invoicing-campaign-list')
|
||||||
|
)
|
||||||
|
assert anchor.text().startswith('Campaigns')
|
|
@ -0,0 +1,296 @@
|
||||||
|
import datetime
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Pool, Regie
|
||||||
|
from tests.utils import login
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_campaign_home(app, admin_user):
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/invoicing/campaigns/')
|
||||||
|
h2 = resp.pyquery('div#appbar h2')
|
||||||
|
assert h2.text() == 'Campaigns'
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_campaign(app, admin_user):
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/')
|
||||||
|
resp = resp.click('Invoicing')
|
||||||
|
resp = resp.click(href='/manage/invoicing/campaigns/')
|
||||||
|
resp = resp.click('New campaign')
|
||||||
|
resp.form['date_start'] = '2022-09-01'
|
||||||
|
resp.form['date_end'] = '2022-10-01'
|
||||||
|
resp.form['date_issue'] = '2022-10-31'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
campaign = Campaign.objects.latest('pk')
|
||||||
|
assert resp.location.endswith('/manage/invoicing/campaign/%s/' % campaign.pk)
|
||||||
|
assert campaign.date_start == datetime.date(2022, 9, 1)
|
||||||
|
assert campaign.date_end == datetime.date(2022, 10, 1)
|
||||||
|
assert campaign.date_issue == datetime.date(2022, 10, 31)
|
||||||
|
|
||||||
|
resp = app.get('/manage/invoicing/campaign/add/')
|
||||||
|
resp.form['date_start'] = '2022-08-31'
|
||||||
|
resp.form['date_end'] = '2022-09-02'
|
||||||
|
resp.form['date_issue'] = '2022-10-31'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert resp.context['form'].errors['__all__'] == [
|
||||||
|
'Another campaign overlapping this period already exists.'
|
||||||
|
]
|
||||||
|
resp.form['date_end'] = '2022-09-01'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
campaign = Campaign.objects.latest('pk')
|
||||||
|
assert campaign.date_start == datetime.date(2022, 8, 31)
|
||||||
|
assert campaign.date_end == datetime.date(2022, 9, 1)
|
||||||
|
assert campaign.date_issue == datetime.date(2022, 10, 31)
|
||||||
|
|
||||||
|
resp = app.get('/manage/invoicing/campaign/add/')
|
||||||
|
resp.form['date_start'] = '2022-09-30'
|
||||||
|
resp.form['date_end'] = '2022-10-02'
|
||||||
|
resp.form['date_issue'] = '2022-10-31'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert resp.context['form'].errors['__all__'] == [
|
||||||
|
'Another campaign overlapping this period already exists.'
|
||||||
|
]
|
||||||
|
resp.form['date_start'] = '2022-10-01'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
campaign = Campaign.objects.latest('pk')
|
||||||
|
assert campaign.date_start == datetime.date(2022, 10, 1)
|
||||||
|
assert campaign.date_end == datetime.date(2022, 10, 2)
|
||||||
|
assert campaign.date_issue == datetime.date(2022, 10, 31)
|
||||||
|
|
||||||
|
|
||||||
|
def test_detail_campaign(app, admin_user):
|
||||||
|
campaign = Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 9, 1),
|
||||||
|
date_end=datetime.date(2022, 10, 1),
|
||||||
|
date_issue=datetime.date(2022, 10, 31),
|
||||||
|
)
|
||||||
|
pool1 = Pool.objects.create(
|
||||||
|
campaign=campaign,
|
||||||
|
draft=True,
|
||||||
|
)
|
||||||
|
pool2 = Pool.objects.create(
|
||||||
|
campaign=campaign,
|
||||||
|
draft=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/invoicing/campaigns/')
|
||||||
|
resp = resp.click(href='/manage/invoicing/campaign/%s/' % campaign.pk)
|
||||||
|
assert '/manage/invoicing/campaign/%s/edit/' % campaign.pk in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/delete/' % campaign.pk in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, pool1.pk) in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, pool2.pk) in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/add/' % (campaign.pk) in resp
|
||||||
|
|
||||||
|
pool3 = Pool.objects.create(
|
||||||
|
campaign=campaign,
|
||||||
|
draft=False,
|
||||||
|
)
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/' % campaign.pk)
|
||||||
|
assert '/manage/invoicing/campaign/%s/edit/' % campaign.pk not in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/delete/' % campaign.pk not in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, pool1.pk) in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, pool2.pk) in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, pool3.pk) in resp
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/add/' % (campaign.pk) not in resp
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_campaign(app, admin_user):
|
||||||
|
campaign = Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 9, 1),
|
||||||
|
date_end=datetime.date(2022, 10, 1),
|
||||||
|
date_issue=datetime.date(2022, 10, 31),
|
||||||
|
)
|
||||||
|
Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 10, 1),
|
||||||
|
date_end=datetime.date(2022, 11, 1),
|
||||||
|
date_issue=datetime.date(2022, 11, 30),
|
||||||
|
)
|
||||||
|
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/edit/' % campaign.pk)
|
||||||
|
resp.form['date_start'] = '2022-09-30'
|
||||||
|
resp.form['date_end'] = '2022-10-02'
|
||||||
|
resp.form['date_issue'] = '2022-12-31'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert resp.context['form'].errors['__all__'] == [
|
||||||
|
'Another campaign overlapping this period already exists.'
|
||||||
|
]
|
||||||
|
resp.form['date_end'] = '2022-10-01'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert resp.location.endswith('/manage/invoicing/campaign/%s/' % campaign.pk)
|
||||||
|
campaign.refresh_from_db()
|
||||||
|
assert campaign.date_start == datetime.date(2022, 9, 30)
|
||||||
|
assert campaign.date_end == datetime.date(2022, 10, 1)
|
||||||
|
assert campaign.date_issue == datetime.date(2022, 12, 31)
|
||||||
|
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/edit/' % campaign.pk)
|
||||||
|
resp.form['date_start'] = '2022-10-31'
|
||||||
|
resp.form['date_end'] = '2022-11-02'
|
||||||
|
resp.form['date_issue'] = '2022-12-31'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert resp.context['form'].errors['__all__'] == [
|
||||||
|
'Another campaign overlapping this period already exists.'
|
||||||
|
]
|
||||||
|
resp.form['date_start'] = '2022-11-01'
|
||||||
|
resp = resp.form.submit()
|
||||||
|
campaign.refresh_from_db()
|
||||||
|
assert campaign.date_start == datetime.date(2022, 11, 1)
|
||||||
|
assert campaign.date_end == datetime.date(2022, 11, 2)
|
||||||
|
assert campaign.date_issue == datetime.date(2022, 12, 31)
|
||||||
|
|
||||||
|
pool = Pool.objects.create(
|
||||||
|
campaign=campaign,
|
||||||
|
draft=True,
|
||||||
|
)
|
||||||
|
app.get('/manage/invoicing/campaign/%s/edit/' % campaign.pk)
|
||||||
|
pool.draft = False
|
||||||
|
pool.save()
|
||||||
|
app.get('/manage/invoicing/campaign/%s/edit/' % campaign.pk, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_campaign(app, admin_user):
|
||||||
|
campaign = Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 9, 1),
|
||||||
|
date_end=datetime.date(2022, 10, 1),
|
||||||
|
date_issue=datetime.date(2022, 10, 31),
|
||||||
|
)
|
||||||
|
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/delete/' % campaign.pk)
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert Campaign.objects.count() == 0
|
||||||
|
assert resp.location.endswith('/manage/invoicing/campaigns/')
|
||||||
|
|
||||||
|
campaign.save()
|
||||||
|
pool = Pool.objects.create(
|
||||||
|
campaign=campaign,
|
||||||
|
draft=True,
|
||||||
|
)
|
||||||
|
regie = Regie.objects.create(label='Foo')
|
||||||
|
invoice = DraftInvoice.objects.create(date_issue=datetime.date.today(), regie=regie, pool=pool)
|
||||||
|
DraftInvoiceLine.objects.create(
|
||||||
|
invoice=invoice,
|
||||||
|
quantity=1,
|
||||||
|
unit_amount=1,
|
||||||
|
total_amount=1,
|
||||||
|
status='success',
|
||||||
|
pool=pool,
|
||||||
|
)
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/delete/' % campaign.pk)
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert Campaign.objects.count() == 0
|
||||||
|
|
||||||
|
campaign.save()
|
||||||
|
pool.draft = False
|
||||||
|
pool.save()
|
||||||
|
app.get('/manage/invoicing/campaign/%s/delete/' % campaign.pk, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_pool(app, admin_user):
|
||||||
|
campaign = Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 9, 1),
|
||||||
|
date_end=datetime.date(2022, 10, 1),
|
||||||
|
date_issue=datetime.date(2022, 10, 31),
|
||||||
|
)
|
||||||
|
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/pool/add/' % campaign.pk)
|
||||||
|
assert resp.form['draft'].value == 'on'
|
||||||
|
with mock.patch('lingo.invoicing.views.generate_invoices') as mock_generate:
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert resp.location.endswith('/manage/invoicing/campaign/%s/#open:pools' % campaign.pk)
|
||||||
|
assert mock_generate.call_args_list == [mock.call(campaign=campaign, draft=True)]
|
||||||
|
|
||||||
|
pool = Pool.objects.create(
|
||||||
|
campaign=campaign,
|
||||||
|
draft=True,
|
||||||
|
)
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/pool/add/' % campaign.pk)
|
||||||
|
resp.form['draft'] = False
|
||||||
|
with mock.patch('lingo.invoicing.views.generate_invoices') as mock_generate:
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert resp.location.endswith('/manage/invoicing/campaign/%s/#open:pools' % campaign.pk)
|
||||||
|
assert mock_generate.call_args_list == [mock.call(campaign=campaign, draft=False)]
|
||||||
|
|
||||||
|
pool.draft = False
|
||||||
|
pool.save()
|
||||||
|
app.get('/manage/invoicing/campaign/%s/pool/add/' % campaign.pk, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
def test_detail_pool(app, admin_user):
|
||||||
|
campaign = Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 9, 1),
|
||||||
|
date_end=datetime.date(2022, 10, 1),
|
||||||
|
date_issue=datetime.date(2022, 10, 31),
|
||||||
|
)
|
||||||
|
campaign2 = Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 10, 1),
|
||||||
|
date_end=datetime.date(2022, 11, 1),
|
||||||
|
date_issue=datetime.date(2022, 11, 30),
|
||||||
|
)
|
||||||
|
pool = Pool.objects.create(
|
||||||
|
campaign=campaign,
|
||||||
|
draft=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, pool.pk))
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/%s/delete/' % (campaign.pk, pool.pk) in resp
|
||||||
|
|
||||||
|
app.get('/manage/invoicing/campaign/%s/pool/%s/' % (0, pool.pk), status=404)
|
||||||
|
app.get('/manage/invoicing/campaign/%s/pool/%s/' % (campaign2.pk, pool.pk), status=404)
|
||||||
|
app.get('/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, 0), status=404)
|
||||||
|
|
||||||
|
pool.draft = False
|
||||||
|
pool.save()
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, pool.pk))
|
||||||
|
assert '/manage/invoicing/campaign/%s/pool/%s/delete/' % (campaign.pk, pool.pk) not in resp
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_pool(app, admin_user):
|
||||||
|
campaign = Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 9, 1),
|
||||||
|
date_end=datetime.date(2022, 10, 1),
|
||||||
|
date_issue=datetime.date(2022, 10, 31),
|
||||||
|
)
|
||||||
|
campaign2 = Campaign.objects.create(
|
||||||
|
date_start=datetime.date(2022, 10, 1),
|
||||||
|
date_end=datetime.date(2022, 11, 1),
|
||||||
|
date_issue=datetime.date(2022, 11, 30),
|
||||||
|
)
|
||||||
|
pool = Pool.objects.create(
|
||||||
|
campaign=campaign,
|
||||||
|
draft=True,
|
||||||
|
)
|
||||||
|
regie = Regie.objects.create(label='Foo')
|
||||||
|
invoice = DraftInvoice.objects.create(date_issue=datetime.date.today(), regie=regie, pool=pool)
|
||||||
|
DraftInvoiceLine.objects.create(
|
||||||
|
invoice=invoice,
|
||||||
|
quantity=1,
|
||||||
|
unit_amount=1,
|
||||||
|
total_amount=1,
|
||||||
|
status='success',
|
||||||
|
pool=pool,
|
||||||
|
)
|
||||||
|
|
||||||
|
app = login(app)
|
||||||
|
resp = app.get('/manage/invoicing/campaign/%s/pool/%s/delete/' % (campaign.pk, pool.pk))
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert Pool.objects.count() == 0
|
||||||
|
assert resp.location.endswith('/manage/invoicing/campaign/%s/#open:pools' % campaign.pk)
|
||||||
|
|
||||||
|
pool.draft = True
|
||||||
|
pool.save()
|
||||||
|
app.get('/manage/invoicing/campaign/%s/pool/%s/delete/' % (0, pool.pk), status=404)
|
||||||
|
app.get('/manage/invoicing/campaign/%s/pool/%s/delete/' % (campaign2.pk, pool.pk), status=404)
|
||||||
|
app.get('/manage/invoicing/campaign/%s/pool/%s/delete/' % (campaign.pk, 0), status=404)
|
||||||
|
|
||||||
|
pool.draft = False
|
||||||
|
pool.save()
|
||||||
|
app.get('/manage/invoicing/campaign/%s/pool/%s/delete/' % (campaign.pk, pool.pk), status=404)
|
|
@ -13,24 +13,6 @@ from tests.utils import login
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_manager_home_show_invoicing(app, admin_user):
|
|
||||||
app = login(app)
|
|
||||||
resp = app.get('/manage/')
|
|
||||||
anchor = resp.pyquery('div#appbar span.actions a[href="%s"]' % reverse('lingo-manager-invoicing-home'))
|
|
||||||
assert anchor.text() == 'Invoicing'
|
|
||||||
|
|
||||||
|
|
||||||
def test_manager_invoicing_home(app, admin_user):
|
|
||||||
app = login(app)
|
|
||||||
resp = app.get(reverse('lingo-manager-invoicing-home'))
|
|
||||||
h2 = resp.pyquery('div#appbar h2')
|
|
||||||
assert h2.text() == 'Invoicing'
|
|
||||||
anchor = resp.pyquery(
|
|
||||||
'div#lingo-manager-main div a[href="%s"]' % reverse('lingo-manager-invoicing-regie-list')
|
|
||||||
)
|
|
||||||
assert anchor.text().startswith('Regies')
|
|
||||||
|
|
||||||
|
|
||||||
def test_manager_invoicing_regie_list_title(app, admin_user):
|
def test_manager_invoicing_regie_list_title(app, admin_user):
|
||||||
app = login(app)
|
app = login(app)
|
||||||
resp = app.get(reverse('lingo-manager-invoicing-regie-list'))
|
resp = app.get(reverse('lingo-manager-invoicing-regie-list'))
|
Loading…
Reference in New Issue
Pour ce template et les suivants, il faut ajouter une ligne dans MANIFEST.in genre
recursive-include lingo/invoicing/templates *.html *.txt
oui c'est https://gitea.entrouvert.org/entrouvert/lingo/pulls/5 :)