Facturation: verrouillage auto des pointages (#75556) #42

Merged
lguerin merged 3 commits from wip/75556-invoicing-lock-events-check into main 2023-04-11 16:04:55 +02:00
11 changed files with 496 additions and 11 deletions

View File

@ -39,14 +39,15 @@ def get_chrono_service():
return list(settings.KNOWN_SERVICES.get('chrono').values())[0]
def get_chrono_json(path, params=None, log_errors=True):
def chrono_json(path, params=None, json_params=None, log_errors=True, method='get'):
chrono_site = get_chrono_service()
if chrono_site is None:
return
try:
response = requests.get(
response = getattr(requests, method)(
path,
params=params or {},
json=json_params or {},
remote_service=chrono_site,
without_user=True,
headers={'accept': 'application/json'},
@ -65,7 +66,7 @@ def get_chrono_json(path, params=None, log_errors=True):
def collect_agenda_data():
result = get_chrono_json('api/agenda/')
result = chrono_json('api/agenda/')
if result is None:
return
if result.get('data') is None:
@ -123,7 +124,7 @@ def get_event(event_slug):
def get_events(event_slugs, error_message=None, error_message_with_details=None):
error_message = error_message or _('Unable to get events details')
error_message_with_details = error_message_with_details or _('Unable to get events details (%s)')
result = get_chrono_json('api/agendas/events/', params={'slots': event_slugs})
result = chrono_json('api/agendas/events/', params={'slots': event_slugs})
if not result:
raise ChronoError(error_message)
if result.get('err'):
@ -144,7 +145,7 @@ def get_subscriptions(agenda_slug, user_external_id=None, date_start=None, date_
params['date_end'] = date_end
if params:
url += '?%s' % '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
result = get_chrono_json(url)
result = chrono_json(url)
if not result:
raise ChronoError(_('Unable to get subscription details'))
if result.get('err'):
@ -155,7 +156,7 @@ def get_subscriptions(agenda_slug, user_external_id=None, date_start=None, date_
def get_check_status(agenda_slugs, user_external_id, date_start, date_end):
result = get_chrono_json(
result = chrono_json(
'api/agendas/events/check-status/?user_external_id=%s&agendas=%s&date_start=%s&date_end=%s'
% (
user_external_id,
@ -171,3 +172,37 @@ def get_check_status(agenda_slugs, user_external_id, date_start, date_end):
if 'data' not in result:
raise ChronoError(_('Unable to get check status'))
return result['data']
def lock_events_check(agenda_slugs, date_start, date_end):
result = chrono_json(
'/api/agendas/events/check-lock/',
json_params={
'check_locked': True,
'agendas': ','.join(agenda_slugs),
'date_start': date_start.isoformat(),
'date_end': date_end.isoformat(),
},
method='post',
)
if not result:
raise ChronoError(_('Unable to lock events check'))
if result.get('err'):
raise ChronoError(_('Unable to lock events check (%s)') % result['err_desc'])
def unlock_events_check(agenda_slugs, date_start, date_end):
result = chrono_json(
'/api/agendas/events/check-lock/',
json_params={
'check_locked': False,
'agendas': ','.join(agenda_slugs),
'date_start': date_start.isoformat(),
'date_end': date_end.isoformat(),
},
method='post',
)
if not result:
raise ChronoError(_('Unable to unlock events check'))
if result.get('err'):
raise ChronoError(_('Unable to unlock events check (%s)') % result['err_desc'])

View File

@ -0,0 +1,15 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0021_campaign_agendas'),
]
operations = [
migrations.AddField(
model_name='campaign',
name='invalid',
field=models.BooleanField(default=False),
),
]

View File

@ -27,7 +27,7 @@ from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from lingo.agendas.chrono import ChronoError
from lingo.agendas.chrono import ChronoError, lock_events_check
from lingo.agendas.models import Agenda
from lingo.utils.misc import generate_slug
@ -143,6 +143,7 @@ class Campaign(models.Model):
max_length=10,
)
agendas = models.ManyToManyField(Agenda, related_name='campaigns')
invalid = models.BooleanField(default=False)
def __str__(self):
return _('%(label)s (%(start)s - %(end)s)') % {
@ -151,8 +152,20 @@ class Campaign(models.Model):
'end': date_format(self.date_end, 'd/m/Y'),
}
def mark_as_valid(self):
self.invalid = False
self.save()
def mark_as_invalid(self):
self.invalid = True
self.save()
def generate(self, spool=True):
pool = self.pool_set.create(draft=True)
try:
pool.init()
except Exception:
return
if spool and 'uwsgi' in sys.modules:
from lingo.invoicing.spooler import generate_invoices
@ -189,6 +202,30 @@ class Pool(models.Model):
def is_last(self):
return not self.campaign.pool_set.filter(created_at__gt=self.created_at).exists()
def init(self):
from lingo.invoicing import utils
try:
agendas = utils.get_agendas(pool=self)
if agendas:
lock_events_check(
agenda_slugs=[a.slug for a in agendas],
date_start=self.campaign.date_start,
date_end=self.campaign.date_end,
)
except ChronoError as e:
self.status = 'failed'
self.exception = e.msg
self.completed_at = now()
self.save()
raise
except Exception:
self.status = 'failed'
self.exception = traceback.format_exc()
self.completed_at = now()
self.save()
raise
def generate_invoices(self):
from lingo.invoicing import utils

View File

@ -70,6 +70,9 @@
{% if not has_running_pool and not has_real_pool %}
<div class="panel--buttons">
<a class="pk-button" rel="popup" href="{% url 'lingo-manager-invoicing-pool-add' regie_pk=regie.pk pk=object.pk %}">{% trans 'Start a pool' %}</a>
{% if not object.invalid %}
pmarillonnet marked this conversation as resolved
Review

Question d’UI, peut-être juste ici indiquer que le déverrouillage n’est pas possible car la campagne est invalide ? Je sens déjà venir les tickets clients “Le bouton de déverrouillage a disparu !!” :)

Question d’UI, peut-être juste ici indiquer que le déverrouillage n’est pas possible car la campagne est invalide ? Je sens déjà venir les tickets clients “Le bouton de déverrouillage a disparu !!” :)
Review

On a déjà un warning en haut de page qui indique qu'il faut relancer un pool, je pense que ça suffit, non ?

On a déjà un warning en haut de page qui indique qu'il faut relancer un pool, je pense que ça suffit, non ?
Review

Ah oui ok j’avais oublié cela, merci.

Ah oui ok j’avais oublié cela, merci.
<a class="pk-button" rel="popup" href="{% url 'lingo-manager-invoicing-campaign-unlock-check' regie_pk=regie.pk pk=object.pk %}">{% trans 'Unlock check' %}</a>
{% endif %}
</div>
{% endif %}
</div>

View File

@ -0,0 +1,22 @@
{% extends "lingo/invoicing/manager_campaign_detail.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-campaign-unlock-check' regie.pk object.pk %}">{% trans "Unlock check" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Unlock check" %}</h2>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
<p>{% trans "Are you sure you want to unlock check?" %}</p>
<div class="buttons">
<button class="submit-button">{% trans "Unlock check" %}</button>
<a class="cancel" href="{% url 'lingo-manager-invoicing-campaign-detail' regie.pk object.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -18,13 +18,19 @@
{% if pool.draft and pool.status != 'registered' and pool.status != 'running' %}
<a href="{% url 'lingo-manager-invoicing-pool-delete' regie_pk=regie.pk pk=object.pk pool_pk=pool.pk %}" rel="popup">{% trans "Delete" %}</a>
{% endif %}
{% if pool.draft and pool.status == 'completed' and pool.is_last %}
{% if not object.invalid and pool.draft and pool.status == 'completed' and pool.is_last %}
<a href="{% url 'lingo-manager-invoicing-pool-promote' regie_pk=regie.pk pk=object.pk pool_pk=pool.pk %}" rel="popup">{% trans "Promote" %}</a>
{% endif %}
</span>
{% endblock %}
{% block content %}
{% if pool.status == 'failed' %}
<div class="pk-error">
<p>{% trans "Error while running pool." %}</p>
{% if pool.exception %}<pre>{{ pool.exception }}</pre>{% endif %}
</div>
{% endif %}
<div class="section">
<div>
<form class="invoice-filters">

View File

@ -63,6 +63,11 @@ urlpatterns = [
views.campaign_delete,
name='lingo-manager-invoicing-campaign-delete',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/unlock-check/',
views.campaign_unlock_check,
name='lingo-manager-invoicing-campaign-unlock-check',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/pool/add/',
views.pool_add,

View File

@ -37,6 +37,7 @@ from django.views.generic import (
UpdateView,
)
from lingo.agendas.chrono import ChronoError, unlock_events_check
from lingo.agendas.models import Agenda
from lingo.invoicing.forms import (
CampaignForm,
@ -284,6 +285,8 @@ class CampaignDetailView(DetailView):
).order_by('-created_at')
kwargs['has_running_pool'] = any(p.status in ['registered', 'running'] for p in kwargs['pools'])
kwargs['has_real_pool'] = any(not p.draft for p in kwargs['pools'])
if self.object.invalid:
messages.warning(self.request, _('The last pool is invalid, please start a new pool.'))
pmarillonnet marked this conversation as resolved
Review

ici le warning

ici le warning
Review

Nickel.

Nickel.
return super().get_context_data(**kwargs)
@ -350,6 +353,47 @@ class CampaignDeleteView(DeleteView):
campaign_delete = CampaignDeleteView.as_view()
class CampaignUnlockCheckView(FormView):
template_name = 'lingo/invoicing/manager_campaign_unlock_check.html'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.object = get_object_or_404(
Campaign.objects.filter(regie=self.regie, invalid=False)
.exclude(pool__draft=False)
.exclude(pool__status__in=['registered', 'running']),
pk=kwargs['pk'],
)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['form'] = None
kwargs['regie'] = self.regie
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
self.object.mark_as_invalid()
agendas = [a.slug for a in self.object.agendas.all()]
if agendas:
try:
unlock_events_check(
agenda_slugs=agendas,
date_start=self.object.date_start,
date_end=self.object.date_end,
)
except ChronoError as e:
messages.error(self.request, _('Fail to unlock events check: %s') % e)
return redirect(
'%s#open:pools'
% reverse('lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.object.pk])
)
campaign_unlock_check = CampaignUnlockCheckView.as_view()
class PoolDetailView(ListView):
template_name = 'lingo/invoicing/manager_pool_detail.html'
paginate_by = 100
@ -454,6 +498,7 @@ class PoolAddView(FormView):
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
self.object.mark_as_valid()
self.object.generate()
return redirect(
'%s#open:pools'
@ -473,6 +518,7 @@ class PoolPromoteView(FormView):
Pool,
campaign__id=kwargs['pk'],
campaign__regie=self.regie,
campaign__invalid=False,
pk=kwargs['pool_pk'],
draft=True,
status='completed',
@ -521,6 +567,8 @@ class PoolDeleteView(DeleteView):
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.is_last:
pmarillonnet marked this conversation as resolved
Review

Pareil ici niveau interface, est-ce qu’il y a l’info qui remonte sur le fait que la campagne est invalide parce que le dernier pool a été supprimé ?

Pareil ici niveau interface, est-ce qu’il y a l’info qui remonte sur le fait que la campagne est invalide parce que le dernier pool a été supprimé ?
Review

oui, un warning :)

oui, un warning :)
self.campaign.mark_as_invalid()
DraftInvoiceLine.objects.filter(pool=self.object).delete()
DraftInvoice.objects.filter(pool=self.object).delete()
return super().delete(request, *args, **kwargs)

View File

@ -13,7 +13,9 @@ from lingo.agendas.chrono import (
get_event,
get_events,
get_subscriptions,
lock_events_check,
refresh_agendas,
unlock_events_check,
)
from lingo.agendas.models import Agenda
@ -458,3 +460,147 @@ def test_get_check_status():
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
) == ['foo', 'bar']
def test_lock_events_check_no_service(settings):
settings.KNOWN_SERVICES = {}
with pytest.raises(ChronoError) as e:
lock_events_check(
agenda_slugs=['foo'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to lock events check'
settings.KNOWN_SERVICES = {'other': []}
with pytest.raises(ChronoError) as e:
lock_events_check(
agenda_slugs=['foo'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to lock events check'
def test_lock_events_check_status():
with mock.patch('requests.Session.post') as requests_post:
requests_post.side_effect = ConnectionError()
with pytest.raises(ChronoError) as e:
lock_events_check(
agenda_slugs=['foo', 'bar'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to lock events check'
with mock.patch('requests.Session.post') as requests_post:
mock_resp = Response()
mock_resp.status_code = 500
requests_post.return_value = mock_resp
with pytest.raises(ChronoError) as e:
lock_events_check(
agenda_slugs=['foo', 'bar'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to lock events check'
with mock.patch('requests.Session.post') as requests_post:
mock_resp = Response()
mock_resp.status_code = 404
requests_post.return_value = mock_resp
with pytest.raises(ChronoError) as e:
lock_events_check(
agenda_slugs=['foo', 'bar'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to lock events check'
with mock.patch('requests.Session.post') as requests_post:
requests_post.return_value = MockedRequestResponse(content=json.dumps({'foo': 'bar'}))
lock_events_check(
agenda_slugs=['foo', 'bar'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert requests_post.call_args_list[0][0] == ('/api/agendas/events/check-lock/',)
assert requests_post.call_args_list[0][1]['json'] == {
'check_locked': True,
'agendas': 'foo,bar',
'date_start': '2022-09-01',
'date_end': '2022-10-01',
}
assert requests_post.call_args_list[0][1]['remote_service']['url'] == 'http://chrono.example.org'
def test_unlock_events_check_no_service(settings):
settings.KNOWN_SERVICES = {}
with pytest.raises(ChronoError) as e:
unlock_events_check(
agenda_slugs=['foo'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to unlock events check'
settings.KNOWN_SERVICES = {'other': []}
with pytest.raises(ChronoError) as e:
unlock_events_check(
agenda_slugs=['foo'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to unlock events check'
def test_unlock_events_check_status():
with mock.patch('requests.Session.post') as requests_post:
requests_post.side_effect = ConnectionError()
with pytest.raises(ChronoError) as e:
unlock_events_check(
agenda_slugs=['foo', 'bar'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to unlock events check'
with mock.patch('requests.Session.post') as requests_post:
mock_resp = Response()
mock_resp.status_code = 500
requests_post.return_value = mock_resp
with pytest.raises(ChronoError) as e:
unlock_events_check(
agenda_slugs=['foo', 'bar'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to unlock events check'
with mock.patch('requests.Session.post') as requests_post:
mock_resp = Response()
mock_resp.status_code = 404
requests_post.return_value = mock_resp
with pytest.raises(ChronoError) as e:
unlock_events_check(
agenda_slugs=['foo', 'bar'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert str(e.value) == 'Unable to unlock events check'
with mock.patch('requests.Session.post') as requests_post:
requests_post.return_value = MockedRequestResponse(content=json.dumps({'foo': 'bar'}))
unlock_events_check(
agenda_slugs=['foo', 'bar'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
assert requests_post.call_args_list[0][0] == ('/api/agendas/events/check-lock/',)
assert requests_post.call_args_list[0][1]['json'] == {
'check_locked': False,
'agendas': 'foo,bar',
'date_start': '2022-09-01',
'date_end': '2022-10-01',
}
assert requests_post.call_args_list[0][1]['remote_service']['url'] == 'http://chrono.example.org'

View File

@ -4,6 +4,7 @@ from unittest import mock
import pytest
from lingo.agendas.chrono import ChronoError
from lingo.agendas.models import Agenda
from lingo.invoicing.models import (
Campaign,
@ -216,6 +217,7 @@ def test_detail_campaign(app, admin_user):
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool1.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool2.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/add/' % (regie.pk, campaign.pk) not in resp
assert '/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk) not in resp
pool2.status = 'running'
pool2.save()
@ -225,6 +227,7 @@ def test_detail_campaign(app, admin_user):
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool1.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool2.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/add/' % (regie.pk, campaign.pk) not in resp
assert '/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk) not in resp
pool2.status = 'failed'
pool2.save()
@ -234,10 +237,12 @@ def test_detail_campaign(app, admin_user):
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool1.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool2.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/add/' % (regie.pk, campaign.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk) in resp
pool3 = Pool.objects.create(
campaign=campaign,
draft=False,
status='completed',
)
resp = app.get('/manage/invoicing/regie/%s/campaign/%s/' % (regie.pk, campaign.pk))
assert '/manage/invoicing/regie/%s/campaign/%s/edit/' % (regie.pk, campaign.pk) not in resp
@ -246,6 +251,22 @@ def test_detail_campaign(app, admin_user):
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool2.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool3.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/add/' % (regie.pk, campaign.pk) not in resp
assert '/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk) not in resp
assert 'The last pool is invalid, please start a new pool.' not in resp
pool3.draft = True
pool3.save()
campaign.invalid = True
campaign.save()
resp = app.get('/manage/invoicing/regie/%s/campaign/%s/' % (regie.pk, campaign.pk))
assert '/manage/invoicing/regie/%s/campaign/%s/edit/' % (regie.pk, campaign.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/delete/' % (regie.pk, campaign.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool1.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool2.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool3.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/pool/add/' % (regie.pk, campaign.pk) in resp
assert '/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk) not in resp
assert 'The last pool is invalid, please start a new pool.' in resp
line = DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
@ -538,6 +559,7 @@ def test_add_pool(app, admin_user):
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
date_issue=datetime.date(2022, 10, 31),
invalid=True,
)
app = login(app)
@ -548,6 +570,8 @@ def test_add_pool(app, admin_user):
'/manage/invoicing/regie/%s/campaign/%s/#open:pools' % (regie.pk, campaign.pk)
)
assert mock_generate.call_args_list == [mock.call(campaign)]
campaign.refresh_from_db()
assert campaign.invalid is False
pool = Pool.objects.create(
campaign=campaign,
@ -576,6 +600,93 @@ def test_add_pool(app, admin_user):
app.get('/manage/invoicing/regie/%s/campaign/%s/pool/add/' % (regie.pk, campaign.pk), status=404)
@mock.patch('lingo.invoicing.views.unlock_events_check')
def test_unlock_check(mock_unlock, app, admin_user):
regie = Regie.objects.create(label='Foo')
agenda = Agenda.objects.create(label='Foo bar', regie=regie)
agenda2 = Agenda.objects.create(label='Foo bar 2', regie=regie)
Agenda.objects.create(label='Foo bar 3', regie=regie)
campaign = Campaign.objects.create(
regie=regie,
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
date_issue=datetime.date(2022, 10, 31),
)
app = login(app)
# no agendas
resp = app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk))
resp = resp.form.submit()
assert resp.location.endswith(
'/manage/invoicing/regie/%s/campaign/%s/#open:pools' % (regie.pk, campaign.pk)
)
assert mock_unlock.call_args_list == []
campaign.refresh_from_db()
assert campaign.invalid is True
# with agendas
campaign.invalid = False
campaign.save()
campaign.agendas.add(agenda, agenda2)
resp = app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk))
resp = resp.form.submit()
assert resp.location.endswith(
'/manage/invoicing/regie/%s/campaign/%s/#open:pools' % (regie.pk, campaign.pk)
)
assert mock_unlock.call_args_list == [
mock.call(
agenda_slugs=['foo-bar', 'foo-bar-2'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
]
campaign.refresh_from_db()
assert campaign.invalid is True
# ChronoError
campaign.invalid = False
campaign.save()
mock_unlock.side_effect = ChronoError('foo baz')
resp = app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk))
resp = resp.form.submit().follow()
assert 'Fail to unlock events check: foo baz' in resp
campaign.invalid = False
campaign.save()
pool = Pool.objects.create(
campaign=campaign,
draft=True,
status='failed',
)
app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk))
pool.status = 'completed'
pool.save()
app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk))
app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (0, campaign.pk), status=404)
pool.status = 'registered'
pool.save()
app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk), status=404)
pool.status = 'running'
pool.save()
app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk), status=404)
pool.status = 'completed'
pool.draft = False
pool.save()
app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk), status=404)
pool.draft = True
pool.save()
campaign.invalid = True
campaign.save()
app.get('/manage/invoicing/regie/%s/campaign/%s/unlock-check/' % (regie.pk, campaign.pk), status=404)
def test_promote_pool(app, admin_user):
regie = Regie.objects.create(label='Foo')
campaign = Campaign.objects.create(
@ -634,6 +745,15 @@ def test_promote_pool(app, admin_user):
pool.draft = True
pool.save()
campaign.invalid = True
campaign.save()
app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/promote/' % (regie.pk, campaign.pk, pool.pk),
status=404,
)
campaign.invalid = False
campaign.save()
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/promote/' % (regie.pk, campaign.pk, pool.pk)
)
@ -728,6 +848,17 @@ def test_detail_pool(app, admin_user):
pool.status = 'completed'
pool.save()
campaign.invalid = True
campaign.save()
resp = app.get('/manage/invoicing/regie/%s/campaign/%s/pool/%s/' % (regie.pk, campaign.pk, pool.pk))
assert '/manage/invoicing/regie/%s/campaign/%s/pool/%s/delete/' % (regie.pk, campaign.pk, pool.pk) in resp
assert (
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/promote/' % (regie.pk, campaign.pk, pool.pk)
not in resp
)
campaign.invalid = False
campaign.save()
Pool.objects.create(
campaign=pool.campaign,
)
@ -1752,6 +1883,25 @@ def test_delete_pool(app, admin_user):
assert resp.location.endswith(
'/manage/invoicing/regie/%s/campaign/%s/#open:pools' % (regie.pk, campaign.pk)
)
campaign.refresh_from_db()
assert campaign.invalid is True
pool.save()
Pool.objects.create(
campaign=campaign,
draft=True,
status='completed',
)
campaign.invalid = False
campaign.save()
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/delete/' % (regie.pk, campaign.pk, pool.pk)
)
resp.form.submit()
assert Pool.objects.count() == 1
campaign.refresh_from_db()
# pool is not the last, don't invalidate the campaign
assert campaign.invalid is False
pool.draft = True
pool.save()

View File

@ -1838,11 +1838,12 @@ def test_generate_invoices_from_lines():
assert list(invoice2.lines.order_by('pk')) == [line4]
@mock.patch('lingo.invoicing.models.lock_events_check')
@mock.patch('lingo.invoicing.utils.get_agendas')
@mock.patch('lingo.invoicing.utils.get_users_from_subscriptions')
@mock.patch('lingo.invoicing.utils.get_all_invoice_lines')
@mock.patch('lingo.invoicing.utils.generate_invoices_from_lines')
def test_generate_invoices(mock_generate, mock_lines, mock_users, mock_agendas):
def test_generate_invoices(mock_generate, mock_lines, mock_users, mock_agendas, mock_lock):
regie = Regie.objects.create(label='Regie')
agenda1 = Agenda.objects.create(label='Agenda 1')
agenda2 = Agenda.objects.create(label='Agenda 2')
@ -1863,7 +1864,14 @@ def test_generate_invoices(mock_generate, mock_lines, mock_users, mock_agendas):
pool = Pool.objects.latest('pk')
assert pool.campaign == campaign
assert pool.draft is True
assert mock_agendas.call_args_list == [mock.call(pool=pool)]
assert mock_lock.call_args_list == [
mock.call(
agenda_slugs=['agenda-1', 'agenda-2'],
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
)
]
assert mock_agendas.call_args_list == [mock.call(pool=pool), mock.call(pool=pool)]
assert mock_users.call_args_list == [
mock.call(
agendas=[agenda1, agenda2],
@ -1885,11 +1893,12 @@ def test_generate_invoices(mock_generate, mock_lines, mock_users, mock_agendas):
]
@mock.patch('lingo.invoicing.models.lock_events_check')
@mock.patch('lingo.invoicing.utils.get_agendas')
@mock.patch('lingo.invoicing.utils.get_users_from_subscriptions')
@mock.patch('lingo.invoicing.utils.get_all_invoice_lines')
@mock.patch('lingo.invoicing.utils.generate_invoices_from_lines')
def test_generate_invoices_errors(mock_generate, mock_lines, mock_users, mock_agendas):
def test_generate_invoices_errors(mock_generate, mock_lines, mock_users, mock_agendas, mock_lock):
regie = Regie.objects.create(label='Regie')
agenda1 = Agenda.objects.create(label='Agenda 1')
agenda2 = Agenda.objects.create(label='Agenda 2')
@ -1901,6 +1910,15 @@ def test_generate_invoices_errors(mock_generate, mock_lines, mock_users, mock_ag
date_issue=datetime.date(2022, 10, 31),
)
mock_lock.side_effect = ChronoError('foo baz')
mock_agendas.return_value = [agenda1, agenda2]
mock_users.return_value = ['foo', 'bar']
campaign.generate()
pool = Pool.objects.latest('pk')
assert pool.status == 'failed'
assert pool.exception == 'foo baz'
mock_lock.side_effect = None
mock_agendas.return_value = [agenda1, agenda2]
mock_users.side_effect = ChronoError('foo bar')
campaign.generate()