diff --git a/lingo/invoicing/forms.py b/lingo/invoicing/forms.py
new file mode 100644
index 0000000..9d12433
--- /dev/null
+++ b/lingo/invoicing/forms.py
@@ -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 .
+
+
+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)
diff --git a/lingo/invoicing/models.py b/lingo/invoicing/models.py
index 242d6be..131a3ce 100644
--- a/lingo/invoicing/models.py
+++ b/lingo/invoicing/models.py
@@ -19,6 +19,7 @@ import copy
from django.contrib.auth.models import Group
from django.contrib.postgres.fields import JSONField
from django.db import models
+from django.utils.formats import date_format
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
@@ -92,6 +93,12 @@ class Campaign(models.Model):
date_end = models.DateField(_('End 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):
campaign = models.ForeignKey(Campaign, on_delete=models.PROTECT)
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_campaign_detail.html b/lingo/invoicing/templates/lingo/invoicing/manager_campaign_detail.html
new file mode 100644
index 0000000..3f76e5f
--- /dev/null
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_campaign_detail.html
@@ -0,0 +1,57 @@
+{% extends "lingo/invoicing/manager_campaign_list.html" %}
+{% load i18n %}
+
+{% block breadcrumb %}
+ {{ block.super }}
+ {{ object }}
+{% endblock %}
+
+{% block appbar %}
+
{{ object }}
+ {% if not has_real_pool %}
+
+ {% trans "Delete" %}
+ {% trans "Edit" %}
+
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+
+
+
+ {% trans "Settings" %}
+ {% trans "Pools" %}
+
+
+
+
+
+ {% trans "Start date:" %} {{ object.date_start|date:'d/m/Y' }}
+ {% trans "End date:" %} {{ object.date_end|date:'d/m/Y' }}
+ {% trans "Issue date:" %} {{ object.date_issue|date:'d/m/Y' }}
+
+
+
+
+
+ {% if not has_real_pool %}
+
+ {% endif %}
+
+
+
+
+
+{% endblock %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_campaign_form.html b/lingo/invoicing/templates/lingo/invoicing/manager_campaign_form.html
new file mode 100644
index 0000000..a8d5f96
--- /dev/null
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_campaign_form.html
@@ -0,0 +1,35 @@
+{% extends "lingo/invoicing/manager_campaign_list.html" %}
+{% load i18n %}
+
+{% block breadcrumb %}
+ {{ block.super }}
+ {% if form.instance.pk %}
+ {{ object }}
+ {% trans "Edit" %}
+ {% else %}
+ {% trans "New campaign" %}
+ {% endif %}
+{% endblock %}
+
+{% block appbar %}
+ {% if object.pk %}
+ {% trans "Edit campaign" %}
+ {% else %}
+ {% trans "New campaign" %}
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_campaign_list.html b/lingo/invoicing/templates/lingo/invoicing/manager_campaign_list.html
new file mode 100644
index 0000000..b7e3b17
--- /dev/null
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_campaign_list.html
@@ -0,0 +1,42 @@
+{% extends "lingo/invoicing/manager_home.html" %}
+{% load i18n %}
+
+{% block breadcrumb %}
+ {{ block.super }}
+ {% trans "Campaigns" %}
+{% endblock %}
+
+{% block appbar %}
+ {% trans "Campaigns" %}
+
+ {% trans 'New campaign' %}
+
+{% endblock %}
+
+{% block content %}
+
+ {% if object_list %}
+
+
{% trans "Campaigns" %}
+
+
+ {% else %}
+
+ {% 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 %}
+
+ {% endif %}
+{% endblock %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_home.html b/lingo/invoicing/templates/lingo/invoicing/manager_home.html
index b518c5f..1ed09e1 100644
--- a/lingo/invoicing/templates/lingo/invoicing/manager_home.html
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_home.html
@@ -17,6 +17,10 @@
{% trans "Regies" %}
{% trans "Invoicing regies." %}
+
+ {% trans "Campaigns" %}
+ {% trans "Manage invoicing campaigns." %}
+
{% endblock %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_pool_detail.html b/lingo/invoicing/templates/lingo/invoicing/manager_pool_detail.html
new file mode 100644
index 0000000..eb955e2
--- /dev/null
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_pool_detail.html
@@ -0,0 +1,51 @@
+{% extends "lingo/invoicing/manager_campaign_detail.html" %}
+{% load i18n %}
+
+{% block breadcrumb %}
+ {{ block.super }}
+ {% trans "Pool" %}
+{% endblock %}
+
+{% block appbar %}
+ {{ pool.created_at|date:"DATETIME_FORMAT" }}
+ {% if pool.draft %}
+
+ {% trans "Delete" %}
+
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+
+
+
+ {% trans "PK" %}
+ {% trans "Invoice PK" %}
+ {% trans "Label" %}
+ {% trans "Event" %}
+ {% trans "Quantity" %}
+ {% trans "Unit amount" %}
+ {% trans "Total amount" %}
+ {% trans "User" %}
+ {% trans "Payer" %}
+ {% trans "Status" %}
+
+
+
+ {% for line in lines %}
+
+ {{ line.pk }}
+ {{ line.invoice_id|default:'' }}
+ {{ line.label }}
+ {{ line.event.slug }}
+ {{ line.quantity }}
+ {{ line.unit_amount }}
+ {{ line.total_amount }}
+ {{ line.user_external_id }}
+ {{ line.adult_external_id }}
+ {{ line.get_status_display }}
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_pool_form.html b/lingo/invoicing/templates/lingo/invoicing/manager_pool_form.html
new file mode 100644
index 0000000..e590fb1
--- /dev/null
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_pool_form.html
@@ -0,0 +1,22 @@
+{% extends "lingo/invoicing/manager_campaign_detail.html" %}
+{% load i18n %}
+
+{% block breadcrumb %}
+ {{ block.super }}
+ {% trans "Start a pool" %}
+{% endblock %}
+
+{% block appbar %}
+ {% trans "New pool" %}
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/lingo/invoicing/urls.py b/lingo/invoicing/urls.py
index 526954d..31c19de 100644
--- a/lingo/invoicing/urls.py
+++ b/lingo/invoicing/urls.py
@@ -43,4 +43,40 @@ urlpatterns = [
),
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('campaigns/', views.campaign_list, name='lingo-manager-invoicing-campaign-list'),
+ path(
+ 'campaign/add/',
+ views.campaign_add,
+ name='lingo-manager-invoicing-campaign-add',
+ ),
+ path(
+ 'campaign//',
+ views.campaign_detail,
+ name='lingo-manager-invoicing-campaign-detail',
+ ),
+ path(
+ 'campaign//edit/',
+ views.campaign_edit,
+ name='lingo-manager-invoicing-campaign-edit',
+ ),
+ path(
+ 'campaign//delete/',
+ views.campaign_delete,
+ name='lingo-manager-invoicing-campaign-delete',
+ ),
+ path(
+ 'campaign//pool/add/',
+ views.pool_add,
+ name='lingo-manager-invoicing-pool-add',
+ ),
+ path(
+ 'campaign//pool//',
+ views.pool_detail,
+ name='lingo-manager-invoicing-pool-detail',
+ ),
+ path(
+ 'campaign//pool//delete/',
+ views.pool_delete,
+ name='lingo-manager-invoicing-pool-delete',
+ ),
]
diff --git a/lingo/invoicing/views.py b/lingo/invoicing/views.py
index fa75e66..9e4c8d7 100644
--- a/lingo/invoicing/views.py
+++ b/lingo/invoicing/views.py
@@ -21,6 +21,7 @@ import json
from django.contrib import messages
from django.db import transaction
from django.http import HttpResponse
+from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
@@ -35,7 +36,17 @@ from django.views.generic import (
)
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
@@ -175,3 +186,150 @@ class RegiesImportView(FormView):
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()
diff --git a/tests/invoicing/manager/__init__.py b/tests/invoicing/manager/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/invoicing/manager/test_all.py b/tests/invoicing/manager/test_all.py
new file mode 100644
index 0000000..b432b3c
--- /dev/null
+++ b/tests/invoicing/manager/test_all.py
@@ -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')
diff --git a/tests/invoicing/manager/test_campaign.py b/tests/invoicing/manager/test_campaign.py
new file mode 100644
index 0000000..e1e84af
--- /dev/null
+++ b/tests/invoicing/manager/test_campaign.py
@@ -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)
diff --git a/tests/invoicing/test_manager.py b/tests/invoicing/manager/test_regie.py
similarity index 91%
rename from tests/invoicing/test_manager.py
rename to tests/invoicing/manager/test_regie.py
index 09f3d52..6c5acce 100644
--- a/tests/invoicing/test_manager.py
+++ b/tests/invoicing/manager/test_regie.py
@@ -13,24 +13,6 @@ 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')
-
-
def test_manager_invoicing_regie_list_title(app, admin_user):
app = login(app)
resp = app.get(reverse('lingo-manager-invoicing-regie-list'))