promotion d'un pool draft en pool définitif (#73608) #18

Merged
lguerin merged 4 commits from wip/73608-invoicing-promote-draft into main 2023-01-27 16:25:36 +01:00
13 changed files with 841 additions and 70 deletions

View File

@ -44,11 +44,3 @@ class CampaignForm(forms.ModelForm):
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)

View File

@ -28,7 +28,6 @@ class Command(BaseCommand):
parser.add_argument('date_start')
parser.add_argument('date_end')
parser.add_argument('--date-issue')
parser.add_argument('--draft', action='store_true')
def handle(self, *args, **options):
try:
@ -58,7 +57,7 @@ class Command(BaseCommand):
campaign = Campaign.objects.create(
date_start=date_start, date_end=date_end, date_issue=date_issue
)
campaign.generate(spool=False, draft=options['draft'])
campaign.generate(spool=False)
self.stdout.write(
self.style.SUCCESS('Invoicing generation OK (start: %s, end: %s)' % (date_start, date_end))

View File

@ -0,0 +1,48 @@
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0010_event_date'),
]
operations = [
migrations.AddField(
model_name='draftinvoice',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='invoice',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='invoice',
name='number',
field=models.PositiveIntegerField(default=0),
),
migrations.CreateModel(
name='Counter',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name', models.CharField(max_length=128)),
('value', models.PositiveIntegerField(default=0)),
(
'regie',
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.Regie'),
),
],
options={
'unique_together': {('regie', 'name')},
},
),
]

View File

@ -40,6 +40,11 @@ class RegieNotConfigured(Exception):
self.msg = msg
class PoolPromotionError(Exception):
def __init__(self, msg):
self.msg = msg
class Regie(models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
@ -109,8 +114,8 @@ class Campaign(models.Model):
'end': date_format(self.date_end, 'd/m/Y'),
}
def generate(self, spool=True, draft=True):
pool = self.pool_set.create(draft=draft)
def generate(self, spool=True):
pool = self.pool_set.create(draft=True)
if spool and 'uwsgi' in sys.modules:
from lingo.invoicing.spooler import generate_invoices
@ -143,6 +148,10 @@ class Pool(models.Model):
)
exception = models.TextField()
@property
def is_last(self):
return not self.campaign.pool_set.filter(created_at__gt=self.created_at).exists()
def generate_invoices(self):
from lingo.invoicing import utils
@ -170,6 +179,79 @@ class Pool(models.Model):
self.completed_at = now()
self.save()
def promote(self):
if not self.is_last:
# not the last
raise PoolPromotionError('Pool too old')
if not self.draft:
# not a draft
raise PoolPromotionError('Pool is final')
if self.status != 'completed':
# not completed
raise PoolPromotionError('Pool is not completed')
final_pool = copy.deepcopy(self)
final_pool.pk = None
final_pool.draft = False
final_pool.status = 'registered'
final_pool.completed_at = None
final_pool.save()
if 'uwsgi' in sys.modules:
from lingo.invoicing.spooler import populate_from_draft
tenant = getattr(connection, 'tenant', None)
transaction.on_commit(
lambda: populate_from_draft.spool(
draft_pool_id=str(self.pk),
final_pool_id=str(final_pool.pk),
domain=getattr(tenant, 'domain_url', None),
)
)
return
final_pool.populate_from_draft(self)
def populate_from_draft(self, draft_pool):
try:
self.status = 'running'
self.save()
for invoice in draft_pool.draftinvoice_set.all().order_by('pk'):
final_invoice = copy.deepcopy(invoice)
final_invoice.__class__ = Invoice
final_invoice.pk = None
final_invoice.pool = self
final_invoice.number = Counter.get_count(
regie=final_invoice.regie, name=final_invoice.created_at.strftime('%y-%m')
)
final_invoice.save()
for line in invoice.lines.all().order_by('pk'):
final_line = copy.deepcopy(line)
final_line.__class__ = InvoiceLine
final_line.pk = None
final_line.invoice = final_invoice
final_line.pool = self
final_line.save()
for line in draft_pool.draftinvoiceline_set.filter(invoice__isnull=True).order_by('pk'):
final_line = copy.deepcopy(line)
final_line.__class__ = InvoiceLine
final_line.pk = None
final_line.pool = self
final_line.save()
except Exception:
self.status = 'failed'
self.exception = traceback.format_exc()
raise
finally:
if self.status == 'running':
self.status = 'completed'
self.completed_at = now()
self.save()
class AbstractInvoice(models.Model):
label = models.CharField(_('Label'), max_length=300)
@ -177,6 +259,7 @@ class AbstractInvoice(models.Model):
date_issue = models.DateField(_('Issue date'))
regie = models.ForeignKey(Regie, on_delete=models.PROTECT)
payer = models.CharField(_('Payer'), max_length=300)
created_at = models.DateTimeField(auto_now_add=True)
pool = models.ForeignKey(Pool, on_delete=models.PROTECT)
@ -188,8 +271,33 @@ class DraftInvoice(AbstractInvoice):
pass
class Counter(models.Model):
regie = models.ForeignKey(Regie, on_delete=models.PROTECT)
name = models.CharField(max_length=128)
value = models.PositiveIntegerField(default=0)
class Meta:
unique_together = (('regie', 'name'),)
@classmethod
def get_count(cls, regie, name):
queryset = cls.objects.select_for_update()
with transaction.atomic():
counter, dummy = queryset.get_or_create(regie=regie, name=name)
counter.value += 1
counter.save()
return counter.value
class Invoice(AbstractInvoice):
pass
number = models.PositiveIntegerField(default=0)
def format_number(self):
return 'F-%s-%s-%06d' % (
self.regie.slug.upper(),
self.created_at.strftime('%y-%m'),
int(self.number),
)
class InjectedLine(models.Model):

View File

@ -34,8 +34,32 @@ def generate_invoices(args):
set_connection(args['domain'])
try:
pool = Pool.objects.get(campaign__pk=args['campaign_id'], pk=args['pool_id'], status='registered')
pool = Pool.objects.get(
campaign__pk=args['campaign_id'], pk=args['pool_id'], status='registered', draft=True
)
except Pool.DoesNotExist:
return
pool.generate_invoices()
@spool
def populate_from_draft(args):
if args.get('domain'):
# multitenant installation
set_connection(args['domain'])
try:
draft_pool = Pool.objects.get(pk=args['draft_pool_id'], status='completed', draft=True)
except Pool.DoesNotExist:
return
if draft_pool.campaign.pool_set.filter(created_at__gt=draft_pool.created_at, draft=True).exists():
return
try:
final_pool = Pool.objects.get(pk=args['final_pool_id'], status='registered', draft=False)
except Pool.DoesNotExist:
return
final_pool.populate_from_draft(draft_pool)

View File

@ -13,7 +13,7 @@
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<p>{% trans "Are you sure you want to start a new pool?" %}</p>
lguerin marked this conversation as resolved Outdated
Outdated
Review

Pour faire mon intello, il n'y a pas d'espace avant le ? en typo anglaise.

Pour faire mon intello, il n'y a pas d'espace avant le ? en typo anglaise.
<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>

View File

@ -18,6 +18,9 @@
{% if pool.draft and pool.status != 'registered' and pool.status != 'running' %}
<a href="{% url 'lingo-manager-invoicing-pool-delete' pk=object.pk pool_pk=pool.pk %}" rel="popup">{% trans "Delete" %}</a>
{% endif %}
{% if pool.draft and pool.status == 'completed' and pool.is_last %}
<a href="{% url 'lingo-manager-invoicing-pool-promote' pk=object.pk pool_pk=pool.pk %}" rel="popup">{% trans "Promote" %}</a>
{% endif %}
</span>
{% endblock %}
@ -27,7 +30,11 @@
{% ifchanged line.invoice_id %}
{% if not forloop.first %}</ul>{% endif %}
<h3 data-invoice-id="{{ line.invoice_id }}">
{% blocktrans with number=line.invoice_id payer=line.invoice.payer amount=line.invoice.total_amount %}Invoice #{{ number }} addressed to {{ payer }}, amount {{ amount }}€{% endblocktrans %}
{% if pool.draft %}
{% blocktrans with number=line.invoice_id payer=line.invoice.payer amount=line.invoice.total_amount %}Invoice TMP-{{ number }} addressed to {{ payer }}, amount {{ amount }}€{% endblocktrans %}
{% else %}
{% blocktrans with number=line.invoice.format_number payer=line.invoice.payer amount=line.invoice.total_amount %}Invoice {{ number }} addressed to {{ payer }}, amount {{ amount }}€{% endblocktrans %}
{% endif %}
</h3>
<ul class="objects-list" data-invoice-id="{{ line.invoice_id }}">
{% endifchanged %}

View File

@ -0,0 +1,22 @@
{% extends "lingo/invoicing/manager_pool_detail.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-pool-promote' pk=object.pk pool_pk=pool.pk %}">{% trans "Promote" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Promote" %}</h2>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
<p>{% trans "Are you sure you want to promote this pool ? This action is irreversible." %}</p>
<div class="buttons">
<button class="submit-button">{% trans "Promote" %}</button>
<a class="cancel" href="{% url 'lingo-manager-invoicing-pool-detail' pk=object.pk pool_pk=pool.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -79,6 +79,11 @@ urlpatterns = [
views.pool_journal,
name='lingo-manager-invoicing-pool-journal',
),
path(
'campaign/<int:pk>/pool/<int:pool_pk>/promote/',
views.pool_promote,
name='lingo-manager-invoicing-pool-promote',
),
path(
'campaign/<int:pk>/pool/<int:pool_pk>/delete/',
views.pool_delete,

View File

@ -22,8 +22,8 @@ from django.contrib import messages
from django.db import transaction
from django.db.models import Count, IntegerField, OuterRef, Subquery, Value
from django.db.models.functions import Coalesce
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
@ -38,7 +38,7 @@ from django.views.generic import (
)
from lingo.agendas.models import Agenda
from lingo.invoicing.forms import CampaignForm, PoolForm
from lingo.invoicing.forms import CampaignForm
from lingo.invoicing.models import (
Campaign,
DraftInvoice,
@ -354,8 +354,7 @@ pool_journal = PoolJournalView.as_view()
class PoolAddView(FormView):
template_name = 'lingo/invoicing/manager_pool_form.html'
form_class = PoolForm
template_name = 'lingo/invoicing/manager_pool_add.html'
def dispatch(self, request, *args, **kwargs):
self.object = get_object_or_404(
@ -364,26 +363,53 @@ class PoolAddView(FormView):
)
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['form'] = None
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def form_valid(self, form):
self.object.generate(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])
def post(self, request, *args, **kwargs):
self.object.generate()
return redirect(
'%s#open:pools' % reverse('lingo-manager-invoicing-campaign-detail', args=[self.object.pk])
)
pool_add = PoolAddView.as_view()
class PoolPromoteView(FormView):
template_name = 'lingo/invoicing/manager_pool_promote.html'
def dispatch(self, request, *args, **kwargs):
self.object = get_object_or_404(
Pool,
campaign__id=kwargs['pk'],
pk=kwargs['pool_pk'],
draft=True,
status='completed',
)
if not self.object.is_last:
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['form'] = None
kwargs['object'] = self.object.campaign
kwargs['pool'] = self.object
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
self.object.promote()
return redirect(
'%s#open:pools'
% reverse('lingo-manager-invoicing-campaign-detail', args=[self.object.campaign.pk])
)
pool_promote = PoolPromoteView.as_view()
class PoolDeleteView(DeleteView):
template_name = 'lingo/manager_confirm_delete.html'
model = Pool

View File

@ -3,7 +3,7 @@ from unittest import mock
import pytest
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, InvoiceLine, Pool, Regie
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine, Pool, Regie
from tests.utils import login
pytestmark = pytest.mark.django_db
@ -306,23 +306,17 @@ def test_add_pool(app, admin_user):
app = login(app)
resp = app.get('/manage/invoicing/campaign/%s/pool/add/' % campaign.pk)
assert resp.form['draft'].value == 'on'
with mock.patch.object(Campaign, 'generate', autospec=True) 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, draft=True)]
assert mock_generate.call_args_list == [mock.call(campaign)]
pool = Pool.objects.create(
campaign=campaign,
draft=True,
status='failed',
)
resp = app.get('/manage/invoicing/campaign/%s/pool/add/' % campaign.pk)
resp.form['draft'] = False
with mock.patch.object(Campaign, 'generate', autospec=True) 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, draft=False)]
app.get('/manage/invoicing/campaign/%s/pool/add/' % campaign.pk)
pool.status = 'completed'
pool.save()
@ -342,6 +336,53 @@ def test_add_pool(app, admin_user):
app.get('/manage/invoicing/campaign/%s/pool/add/' % campaign.pk, status=404)
def test_promote_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),
)
pool = Pool.objects.create(
campaign=campaign,
draft=True,
status='completed',
)
app = login(app)
resp = app.get('/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk))
with mock.patch.object(Pool, 'promote', autospec=True) as mock_promote:
resp = resp.form.submit()
assert resp.location.endswith('/manage/invoicing/campaign/%s/#open:pools' % campaign.pk)
assert mock_promote.call_args_list == [mock.call(pool)]
pool.status = 'registered'
pool.save()
app.get('/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk), status=404)
pool.status = 'running'
pool.save()
app.get('/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk), status=404)
pool.status = 'failed'
pool.save()
app.get('/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk), status=404)
pool.status = 'completed'
pool.draft = False
pool.save()
app.get('/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk), status=404)
pool.draft = True
pool.save()
resp = app.get('/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk))
resp.form.submit()
assert Pool.objects.filter(draft=False).exists()
# not the last
app.get('/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk), status=404)
def test_detail_pool(app, admin_user):
campaign = Campaign.objects.create(
date_start=datetime.date(2022, 9, 1),
@ -362,6 +403,7 @@ def test_detail_pool(app, admin_user):
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
assert '/manage/invoicing/campaign/%s/pool/%s/promote/' % (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)
@ -371,22 +413,37 @@ def test_detail_pool(app, admin_user):
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
assert '/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk) not in resp
pool.draft = True
pool.status = 'registered'
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
assert '/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk) not in resp
pool.status = 'running'
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
assert '/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk) not in resp
pool.status = 'failed'
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) in resp
assert '/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk) not in resp
pool.status = 'completed'
pool.save()
Pool.objects.create(
campaign=pool.campaign,
)
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
assert (
'/manage/invoicing/campaign/%s/pool/%s/promote/' % (campaign.pk, pool.pk) not in resp
) # not the last
line = DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
@ -446,7 +503,11 @@ def test_detail_pool(app, admin_user):
assert 'tag-error' not in resp
def test_detail_pool_invoices(app, admin_user):
@pytest.mark.parametrize('draft', [True, False])
def test_detail_pool_invoices(app, admin_user, draft):
invoice_model = DraftInvoice if draft else Invoice
line_model = DraftInvoiceLine if draft else InvoiceLine
campaign = Campaign.objects.create(
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
@ -454,18 +515,23 @@ def test_detail_pool_invoices(app, admin_user):
)
pool = Pool.objects.create(
campaign=campaign,
draft=True,
draft=draft,
status='completed',
)
regie = Regie.objects.create(label='Foo')
invoice1 = DraftInvoice.objects.create(
invoice1 = invoice_model.objects.create(
date_issue=datetime.date.today(), regie=regie, pool=pool, payer='payer:1'
)
invoice2 = DraftInvoice.objects.create(
invoice2 = invoice_model.objects.create(
date_issue=datetime.date.today(), regie=regie, pool=pool, payer='payer:2'
)
if not draft:
invoice1.number = 42
invoice1.save()
invoice2.number = 43
invoice2.save()
line11 = DraftInvoiceLine.objects.create(
line11 = line_model.objects.create(
event_date=datetime.date(2022, 9, 1),
invoice=invoice1,
quantity=1,
@ -477,7 +543,7 @@ def test_detail_pool_invoices(app, admin_user):
user_external_id='user:1',
user_name='User1 Name1',
)
line12 = DraftInvoiceLine.objects.create(
line12 = line_model.objects.create(
event_date=datetime.date(2022, 9, 2),
invoice=invoice1,
quantity=1,
@ -489,7 +555,7 @@ def test_detail_pool_invoices(app, admin_user):
user_external_id='user:2',
user_name='User2 Name2',
)
line13 = DraftInvoiceLine.objects.create(
line13 = line_model.objects.create(
event_date=datetime.date(2022, 9, 3),
invoice=invoice1,
quantity=1,
@ -502,7 +568,7 @@ def test_detail_pool_invoices(app, admin_user):
user_name='User1 Name1',
)
orphan_line = DraftInvoiceLine.objects.create(
orphan_line = line_model.objects.create(
event_date=datetime.date(2022, 9, 1),
quantity=1,
unit_amount=42,
@ -514,7 +580,7 @@ def test_detail_pool_invoices(app, admin_user):
user_name='User1 Name1',
)
line21 = DraftInvoiceLine.objects.create(
line21 = line_model.objects.create(
event_date=datetime.date(2022, 9, 1),
invoice=invoice2,
quantity=1,
@ -530,10 +596,17 @@ def test_detail_pool_invoices(app, admin_user):
app = login(app)
resp = app.get('/manage/invoicing/campaign/%s/pool/%s/' % (campaign.pk, pool.pk))
assert '#%s' % orphan_line.pk not in resp
assert (
resp.pyquery('h3[data-invoice-id="%s"]' % invoice1.pk).text()
== 'Invoice #%s addressed to payer:1, amount 6.00€' % invoice1.pk
)
if draft:
assert (
resp.pyquery('h3[data-invoice-id="%s"]' % invoice1.pk).text()
== 'Invoice TMP-%s addressed to payer:1, amount 6.00€' % invoice1.pk
)
else:
assert resp.pyquery(
'h3[data-invoice-id="%s"]' % invoice1.pk
).text() == 'Invoice F-FOO-%s-000042 addressed to payer:1, amount 6.00€' % invoice1.created_at.strftime(
'%y-%m'
)
assert len(resp.pyquery('ul[data-invoice-id="%s"] li' % invoice1.pk)) == 3
assert (
resp.pyquery('ul[data-invoice-id="%s"] li:nth-child(1)' % invoice1.pk).text()
@ -547,10 +620,17 @@ def test_detail_pool_invoices(app, admin_user):
resp.pyquery('ul[data-invoice-id="%s"] li:nth-child(3)' % invoice1.pk).text()
== '#%s User2 Name2 - 02/09/2022 - Label 12 (2.00)' % line12.pk
)
assert (
resp.pyquery('h3[data-invoice-id="%s"]' % invoice2.pk).text()
== 'Invoice #%s addressed to payer:2, amount 1.00€' % invoice2.pk
)
if draft:
assert (
resp.pyquery('h3[data-invoice-id="%s"]' % invoice2.pk).text()
== 'Invoice TMP-%s addressed to payer:2, amount 1.00€' % invoice2.pk
)
else:
assert resp.pyquery(
'h3[data-invoice-id="%s"]' % invoice2.pk
).text() == 'Invoice F-FOO-%s-000043 addressed to payer:2, amount 1.00€' % invoice2.created_at.strftime(
'%y-%m'
)
assert len(resp.pyquery('ul[data-invoice-id="%s"] li' % invoice2.pk)) == 1
assert (
resp.pyquery('ul[data-invoice-id="%s"] li:nth-child(1)' % invoice2.pk).text()

View File

@ -6,17 +6,21 @@ import pytest
from django.core.management import CommandError, call_command
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.utils.timezone import now
from lingo.agendas.chrono import ChronoError
from lingo.agendas.models import Agenda
from lingo.invoicing import utils
from lingo.invoicing.models import (
Campaign,
Counter,
DraftInvoice,
DraftInvoiceLine,
InjectedLine,
Invoice,
InvoiceLine,
Pool,
PoolPromotionError,
Regie,
RegieNotConfigured,
)
@ -1273,7 +1277,7 @@ def test_generate_invoices(mock_generate, mock_lines, mock_users, mock_agendas):
mock_lines.return_value = ['foo', 'baz']
# check only calls between functions
campaign.generate(draft=True)
campaign.generate()
pool = Pool.objects.latest('pk')
assert pool.campaign == campaign
assert pool.draft is True
@ -1316,7 +1320,7 @@ def test_generate_invoices_errors(mock_generate, mock_lines, mock_users, mock_ag
mock_agendas.return_value = [agenda1, agenda2]
mock_users.side_effect = ChronoError('foo bar')
campaign.generate(draft=True)
campaign.generate()
pool = Pool.objects.latest('pk')
assert pool.status == 'failed'
assert pool.exception == 'foo bar'
@ -1324,7 +1328,7 @@ def test_generate_invoices_errors(mock_generate, mock_lines, mock_users, mock_ag
mock_users.side_effect = None
mock_users.return_value = ['foo', 'bar']
mock_lines.side_effect = RegieNotConfigured('foo baz')
campaign.generate(draft=True)
campaign.generate()
pool = Pool.objects.latest('pk')
assert pool.status == 'failed'
assert pool.exception == 'foo baz'
@ -1359,17 +1363,13 @@ def test_generate_invoices_cmd():
assert campaign.date_start == datetime.date(2022, 9, 1)
assert campaign.date_end == datetime.date(2022, 10, 1)
assert campaign.date_issue == campaign.date_end
assert mock_generate.call_args_list == [mock.call(campaign, draft=False, spool=False)]
assert mock_generate.call_args_list == [mock.call(campaign, spool=False)]
mock_generate.reset_mock()
# again
call_command('generate_invoices', '2022-09-01', '2022-10-01')
assert Campaign.objects.count() == 1
assert mock_generate.call_args_list == [mock.call(campaign, draft=False, spool=False)]
mock_generate.reset_mock()
call_command('generate_invoices', '2022-09-01', '2022-10-01', '--draft')
assert Campaign.objects.count() == 1
assert mock_generate.call_args_list == [mock.call(campaign, draft=True, spool=False)]
assert mock_generate.call_args_list == [mock.call(campaign, spool=False)]
mock_generate.reset_mock()
# with overlapping
@ -1386,7 +1386,7 @@ def test_generate_invoices_cmd():
call_command('generate_invoices', '2022-08-01', '2022-09-01', '--date-issue=2022-10-31')
assert Campaign.objects.count() == 2
campaign = Campaign.objects.latest('pk')
assert mock_generate.call_args_list == [mock.call(campaign, draft=False, spool=False)]
assert mock_generate.call_args_list == [mock.call(campaign, spool=False)]
assert campaign.date_start == datetime.date(2022, 8, 1)
assert campaign.date_end == datetime.date(2022, 9, 1)
assert campaign.date_issue == datetime.date(2022, 10, 31)
@ -1394,7 +1394,424 @@ def test_generate_invoices_cmd():
call_command('generate_invoices', '2022-10-01', '2022-11-01')
assert Campaign.objects.count() == 3
campaign = Campaign.objects.latest('pk')
assert mock_generate.call_args_list == [mock.call(campaign, draft=False, spool=False)]
assert mock_generate.call_args_list == [mock.call(campaign, spool=False)]
assert campaign.date_start == datetime.date(2022, 10, 1)
assert campaign.date_end == datetime.date(2022, 11, 1)
assert campaign.date_issue == campaign.date_end
def test_promote_pool():
today = datetime.date.today()
regie1 = Regie.objects.create(label='Regie1')
regie2 = Regie.objects.create(label='Regie2')
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),
)
old_pool = Pool.objects.create(
campaign=campaign,
draft=True,
)
pool = Pool.objects.create(
campaign=campaign,
draft=True,
completed_at=now(),
status='completed',
)
other_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),
)
other_pool = Pool.objects.create(
campaign=other_campaign,
draft=True,
)
invoice1 = DraftInvoice.objects.create(
date_issue=datetime.date.today(), regie=regie1, pool=pool, payer='payer:1'
)
line11 = DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='label-11',
label='Label 11',
quantity=1,
unit_amount=1,
total_amount=1,
user_external_id='user:1',
user_name='User1 Name1',
payer_external_id='payer:1',
status='success',
event={'foo': 'bar'},
pricing_data={'foo': 'baz'},
invoice=invoice1,
pool=pool,
)
injected_line12 = InjectedLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='label-12',
label='Label 12',
quantity=1,
unit_amount=2,
total_amount=2,
user_external_id='user:2',
payer_external_id='payer:1',
regie=regie1,
)
line12 = DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='label-12',
label='Label 12',
quantity=1,
unit_amount=2,
total_amount=2,
user_external_id='user:2',
user_name='User2 Name2',
payer_external_id='payer:1',
status='success',
from_injected_line=injected_line12,
invoice=invoice1,
pool=pool,
)
invoice2 = DraftInvoice.objects.create(
date_issue=datetime.date.today(), regie=regie1, pool=pool, payer='payer:2'
)
injected_line21 = InjectedLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='label-21',
label='Label 21',
quantity=1,
unit_amount=1,
total_amount=1,
user_external_id='user:2',
payer_external_id='payer:2',
regie=regie1,
)
line21 = DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='label-21',
label='Label 21',
quantity=1,
unit_amount=1,
total_amount=1,
user_external_id='user:1',
user_name='User1 Name1',
payer_external_id='payer:2',
status='success',
invoice=invoice2,
pool=pool,
from_injected_line=injected_line21,
)
line22 = DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='label-22',
label='Label 22',
quantity=1,
unit_amount=2,
total_amount=2,
user_external_id='user:2',
user_name='User2 Name2',
payer_external_id='payer:2',
status='success',
invoice=invoice2,
pool=pool,
)
orphan_line1 = DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='orphan-1',
label='Orphan 1',
quantity=0,
unit_amount=0,
total_amount=0,
user_external_id='user:1',
user_name='User1 Name1',
payer_external_id='payer:1',
status='error',
pool=pool,
)
orphan_line2 = DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='orphan-2',
label='Orphan 2',
quantity=0,
unit_amount=0,
total_amount=0,
user_external_id='user:1',
user_name='User1 Name1',
payer_external_id='payer:1',
status='warning',
pool=pool,
)
invoice3 = DraftInvoice.objects.create(
date_issue=datetime.date.today(), regie=regie2, pool=pool, payer='payer:1'
)
old_invoice = DraftInvoice.objects.create(
date_issue=datetime.date.today(), regie=regie1, pool=old_pool, payer='payer:1'
)
DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='old-1',
label='Old 1',
quantity=1,
unit_amount=1,
total_amount=1,
user_external_id='user:1',
user_name='User1 Name1',
payer_external_id='payer:1',
status='success',
invoice=old_invoice,
pool=old_pool,
)
old_injected_line2 = InjectedLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='old-2',
label='Old 2',
quantity=1,
unit_amount=2,
total_amount=2,
user_external_id='user:2',
payer_external_id='payer:1',
regie=regie1,
)
DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='old-2',
label='Old 2',
quantity=1,
unit_amount=2,
total_amount=2,
user_external_id='user:2',
user_name='User2 Name2',
payer_external_id='payer:1',
status='success',
from_injected_line=old_injected_line2,
invoice=old_invoice,
pool=old_pool,
)
other_invoice = DraftInvoice.objects.create(
date_issue=datetime.date.today(), regie=regie1, pool=other_pool, payer='payer:1'
)
DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='other-1',
label='Other 1',
quantity=1,
unit_amount=1,
total_amount=1,
user_external_id='user:1',
user_name='User1 Name1',
payer_external_id='payer:1',
status='success',
invoice=other_invoice,
pool=other_pool,
)
other_injected_line2 = InjectedLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='other-2',
label='Other 2',
quantity=1,
unit_amount=2,
total_amount=2,
user_external_id='user:2',
payer_external_id='payer:1',
regie=regie1,
)
DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='other-2',
label='Other 2',
quantity=1,
unit_amount=2,
total_amount=2,
user_external_id='user:2',
user_name='User2 Name2',
payer_external_id='payer:1',
status='success',
from_injected_line=other_injected_line2,
invoice=other_invoice,
pool=other_pool,
)
# refresh amounts
invoice1.refresh_from_db()
invoice2.refresh_from_db()
assert Campaign.objects.count() == 2
assert Pool.objects.count() == 3
assert Pool.objects.filter(draft=False).count() == 0
assert DraftInvoice.objects.count() == 5
assert DraftInvoiceLine.objects.count() == 10
assert Invoice.objects.count() == 0
assert InvoiceLine.objects.count() == 0
assert InjectedLine.objects.count() == 4
pool.promote()
def test_counts():
assert Campaign.objects.count() == 2
assert Pool.objects.count() == 4
assert Pool.objects.filter(draft=False).count() == 1
assert DraftInvoice.objects.count() == 5
assert DraftInvoiceLine.objects.count() == 10
assert Invoice.objects.count() == 3
assert InvoiceLine.objects.count() == 6
assert InjectedLine.objects.count() == 4
assert Counter.objects.get(regie=regie1, name=today.strftime('%y-%m')).value == 2
assert Counter.objects.get(regie=regie2, name=today.strftime('%y-%m')).value == 1
test_counts()
final_pool = Pool.objects.filter(draft=False).get()
assert final_pool.campaign == campaign
assert final_pool.created_at > pool.created_at
assert final_pool.completed_at > pool.completed_at
assert final_pool.status == 'completed'
assert final_pool.exception == ''
final_invoice1 = Invoice.objects.order_by('pk')[0]
assert final_invoice1.date_issue == invoice1.date_issue
assert final_invoice1.regie == regie1
assert final_invoice1.pool == final_pool
assert final_invoice1.payer == invoice1.payer
assert final_invoice1.total_amount == invoice1.total_amount == 3
assert final_invoice1.number == 1
assert final_invoice1.format_number() == 'F-REGIE1-%s-000001' % today.strftime('%y-%m')
final_line11 = InvoiceLine.objects.order_by('pk')[0]
assert final_line11.event_date == line11.event_date
assert final_line11.slug == line11.slug
assert final_line11.label == line11.label
assert final_line11.quantity == line11.quantity
assert final_line11.unit_amount == line11.unit_amount
assert final_line11.total_amount == line11.total_amount
assert final_line11.user_external_id == line11.user_external_id
assert final_line11.user_name == line11.user_name
assert final_line11.payer_external_id == line11.payer_external_id
assert final_line11.event == line11.event
assert final_line11.pricing_data == line11.pricing_data
assert final_line11.status == line11.status
assert final_line11.pool == final_pool
assert final_line11.invoice == final_invoice1
assert final_line11.from_injected_line is None
final_line12 = InvoiceLine.objects.order_by('pk')[1]
assert final_line12.event_date == line12.event_date
assert final_line12.slug == line12.slug
assert final_line12.label == line12.label
assert final_line12.quantity == line12.quantity
assert final_line12.unit_amount == line12.unit_amount
assert final_line12.total_amount == line12.total_amount
assert final_line12.user_external_id == line12.user_external_id
assert final_line12.user_name == line12.user_name
assert final_line12.payer_external_id == line12.payer_external_id
assert final_line12.event == line12.event
assert final_line12.pricing_data == line12.pricing_data
assert final_line12.status == line12.status
assert final_line12.pool == final_pool
assert final_line12.invoice == final_invoice1
assert final_line12.from_injected_line == injected_line12
final_invoice2 = Invoice.objects.order_by('pk')[1]
assert final_invoice2.date_issue == invoice2.date_issue
assert final_invoice2.regie == regie1
assert final_invoice2.pool == final_pool
assert final_invoice2.payer == invoice2.payer
assert final_invoice2.total_amount == invoice2.total_amount == 3
assert final_invoice2.number == 2
assert final_invoice2.format_number() == 'F-REGIE1-%s-000002' % today.strftime('%y-%m')
final_line21 = InvoiceLine.objects.order_by('pk')[2]
assert final_line21.event_date == line21.event_date
assert final_line21.slug == line21.slug
assert final_line21.label == line21.label
assert final_line21.quantity == line21.quantity
assert final_line21.unit_amount == line21.unit_amount
assert final_line21.total_amount == line21.total_amount
assert final_line21.user_external_id == line21.user_external_id
assert final_line21.user_name == line21.user_name
assert final_line21.payer_external_id == line21.payer_external_id
assert final_line21.event == line21.event
assert final_line21.pricing_data == line21.pricing_data
assert final_line21.status == line21.status
assert final_line21.pool == final_pool
assert final_line21.invoice == final_invoice2
assert final_line21.from_injected_line == injected_line21
final_line22 = InvoiceLine.objects.order_by('pk')[3]
assert final_line22.event_date == line22.event_date
assert final_line22.slug == line22.slug
assert final_line22.label == line22.label
assert final_line22.quantity == line22.quantity
assert final_line22.unit_amount == line22.unit_amount
assert final_line22.total_amount == line22.total_amount
assert final_line22.user_external_id == line22.user_external_id
assert final_line22.user_name == line22.user_name
assert final_line22.payer_external_id == line22.payer_external_id
assert final_line22.event == line22.event
assert final_line22.pricing_data == line22.pricing_data
assert final_line22.status == line22.status
assert final_line22.pool == final_pool
assert final_line22.invoice == final_invoice2
assert final_line22.from_injected_line is None
final_orphan_line1 = InvoiceLine.objects.order_by('pk')[4]
assert final_orphan_line1.event_date == orphan_line1.event_date
assert final_orphan_line1.slug == orphan_line1.slug
assert final_orphan_line1.label == orphan_line1.label
assert final_orphan_line1.quantity == orphan_line1.quantity
assert final_orphan_line1.unit_amount == orphan_line1.unit_amount
assert final_orphan_line1.total_amount == orphan_line1.total_amount
assert final_orphan_line1.user_external_id == orphan_line1.user_external_id
assert final_orphan_line1.user_name == orphan_line1.user_name
assert final_orphan_line1.payer_external_id == orphan_line1.payer_external_id
assert final_orphan_line1.event == orphan_line1.event
assert final_orphan_line1.pricing_data == orphan_line1.pricing_data
assert final_orphan_line1.status == orphan_line1.status
assert final_orphan_line1.pool == final_pool
assert final_orphan_line1.from_injected_line is None
final_orphan_line2 = InvoiceLine.objects.order_by('pk')[5]
assert final_orphan_line2.event_date == orphan_line2.event_date
assert final_orphan_line2.slug == orphan_line2.slug
assert final_orphan_line2.label == orphan_line2.label
assert final_orphan_line2.quantity == orphan_line2.quantity
assert final_orphan_line2.unit_amount == orphan_line2.unit_amount
assert final_orphan_line2.total_amount == orphan_line2.total_amount
assert final_orphan_line2.user_external_id == orphan_line2.user_external_id
assert final_orphan_line2.user_name == orphan_line2.user_name
assert final_orphan_line2.payer_external_id == orphan_line2.payer_external_id
assert final_orphan_line2.event == orphan_line2.event
assert final_orphan_line2.pricing_data == orphan_line2.pricing_data
assert final_orphan_line2.status == orphan_line2.status
assert final_orphan_line2.pool == final_pool
assert final_orphan_line2.from_injected_line is None
final_invoice3 = Invoice.objects.order_by('pk')[2]
assert final_invoice3.date_issue == invoice3.date_issue
assert final_invoice3.regie == regie2
assert final_invoice3.pool == final_pool
assert final_invoice3.payer == invoice3.payer
assert final_invoice3.total_amount == invoice3.total_amount == 0
assert final_invoice3.number == 1
assert final_invoice3.format_number() == 'F-REGIE2-%s-000001' % today.strftime('%y-%m')
with pytest.raises(PoolPromotionError) as excinfo:
old_pool.promote()
assert '%s' % excinfo.value == 'Pool too old'
test_counts()
with pytest.raises(PoolPromotionError) as excinfo:
final_pool.promote()
assert '%s' % excinfo.value == 'Pool is final'
test_counts()
for status in ['registered', 'running', 'failed']:
other_pool.status = status
other_pool.save()
with pytest.raises(PoolPromotionError) as excinfo:
other_pool.promote()
assert '%s' % excinfo.value == 'Pool is not completed'
test_counts()

View File

@ -2,7 +2,16 @@ import datetime
import pytest
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine, Pool, Regie
from lingo.invoicing.models import (
Campaign,
Counter,
DraftInvoice,
DraftInvoiceLine,
Invoice,
InvoiceLine,
Pool,
Regie,
)
pytestmark = pytest.mark.django_db
@ -151,3 +160,37 @@ def test_invoice_total_amount(draft):
assert invoice.total_amount == 12
invoice2.refresh_from_db()
assert invoice2.total_amount == 0
def test_counter():
regie1 = Regie.objects.create()
regie2 = Regie.objects.create()
assert Counter.get_count(regie=regie1, name='foo') == 1
assert Counter.objects.count() == 1
counter1 = Counter.objects.get(regie=regie1, name='foo')
assert counter1.value == 1
assert Counter.get_count(regie=regie1, name='foo') == 2
counter1.refresh_from_db()
assert counter1.value == 2
assert Counter.get_count(regie=regie1, name='foo') == 3
counter1.refresh_from_db()
assert counter1.value == 3
assert Counter.get_count(regie=regie2, name='foo') == 1
assert Counter.objects.count() == 2
counter1.refresh_from_db()
assert counter1.value == 3
counter2 = Counter.objects.get(regie=regie2, name='foo')
assert counter2.value == 1
assert Counter.get_count(regie=regie2, name='bar') == 1
assert Counter.objects.count() == 3
counter1.refresh_from_db()
assert counter1.value == 3
counter2.refresh_from_db()
assert counter2.value == 1
counter3 = Counter.objects.get(regie=regie2, name='bar')
assert counter3.value == 1