invoicing: parametrize counter name and number format (#73744) #20

Merged
lguerin merged 1 commits from wip/73744-invoice-counter into main 2023-02-04 15:32:16 +01:00
11 changed files with 137 additions and 29 deletions

View File

@ -57,9 +57,10 @@ class AbstractLineFilterSet(django_filters.FilterSet):
queryset=regie_queryset,
field_name='invoice__regie',
)
invoice_number = django_filters.NumberFilter(
invoice_number = django_filters.CharFilter(
label=_('Invoice number'),
field_name='invoice__number',
field_name='invoice__formatted_number',
lookup_expr='contains',
)
invoice_id = django_filters.NumberFilter(
label=_('Invoice number'),

View File

@ -0,0 +1,23 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0011_counter'),
]
operations = [
migrations.AddField(
model_name='regie',
name='counter_name',
field=models.CharField(default='{yy}', max_length=50, verbose_name='Counter name'),
),
migrations.AddField(
model_name='regie',
name='number_format',
field=models.CharField(
default='F{regie_id:02d}-{yy}-{mm}-{number:07d}', max_length=100, verbose_name='Number format'
),
),
]

View File

@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0012_counter'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='formatted_number',
field=models.CharField(default='', max_length=200),
preserve_default=False,
),
]

View File

@ -61,6 +61,17 @@ class Regie(models.Model):
on_delete=models.SET_NULL,
)
counter_name = models.CharField(
_('Counter name'),
default='{yy}',
max_length=50,
)
number_format = models.CharField(
_('Number format'),
default='F{regie_id:02d}-{yy}-{mm}-{number:07d}',
lguerin marked this conversation as resolved Outdated

passer à 07d par défaut

passer à 07d par défaut
Outdated
Review

Pour que ça soit en accord avec le counter_name par défaut proposé plus haut, on mettrait pas « F{regie_id:02d}-{yy}-{number:07d}» ?

Ou, autrement, est-ce qu'on ne ferait pas en sorte que « number_format » ne soit pas paramétrable, et soit toujours « F{regie_id:02d}-{counter_name}-{number:07d} » ?... Mais bon, j'imagine que des villes voudront conserver leur numérotation existante, pour ne pas casser de l'existant dans leur système compta à co... et que c'est qui pousse à rendre tout cela paramétrable.

En fait ça me chiffonne juste un peu d'imaginer un jour un counter_name à {yy}-{mm} et un number_format à F{regie_id:02d}-{yy}-{number:07d} et boum. Mais on pourra lever une erreur sur détection d'un numéro de facture en double j'imagine.

Allez, c'était mon dernier commentaire, tu fais comme tu veux pour ce default :)

Pour que ça soit en accord avec le counter_name par défaut proposé plus haut, on mettrait pas « F{regie_id:02d}-{yy}-{number:07d}» ? Ou, autrement, est-ce qu'on ne ferait pas en sorte que « number_format » ne soit pas paramétrable, et soit toujours « F{regie_id:02d}-{counter_name}-{number:07d} » ?... Mais bon, j'imagine que des villes voudront conserver leur numérotation existante, pour ne pas casser de l'existant dans leur système compta à co... et que c'est qui pousse à rendre tout cela paramétrable. En fait ça me chiffonne juste un peu d'imaginer un jour un counter_name à {yy}-{mm} et un number_format à F{regie_id:02d}-{yy}-{number:07d} et boum. Mais on pourra lever une erreur sur détection d'un numéro de facture en double j'imagine. Allez, c'était mon dernier commentaire, tu fais comme tu veux pour ce default :)

Stéphane voudrait un compteur par régie et année (donc counter_name = {yy}, lié à la régié), mais dans le numéro de facture qu'on présente, voir le mois de faturation.
Ca correspond aux defaults posés.
Je dirais que c'est charge à l'admin de paramétrer sa régie correctement :)

Stéphane voudrait un compteur par régie et année (donc counter_name = {yy}, lié à la régié), mais dans le numéro de facture qu'on présente, voir le mois de faturation. Ca correspond aux defaults posés. Je dirais que c'est charge à l'admin de paramétrer sa régie correctement :)
max_length=100,
)
class Meta:
ordering = ['label']
@ -102,6 +113,22 @@ class Regie(models.Model):
regie, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
return created, regie
def get_counter_name(self, invoice_date):
return self.counter_name.format(
yyyy=invoice_date.strftime('%Y'),
yy=invoice_date.strftime('%y'),
mm=invoice_date.strftime('%m'),
)
def format_number(self, invoice_date, invoice_number):
return self.number_format.format(
yyyy=invoice_date.strftime('%Y'),
yy=invoice_date.strftime('%y'),
mm=invoice_date.strftime('%m'),
number=invoice_number,
regie_id=self.pk,
)
class Campaign(models.Model):
date_start = models.DateField(_('Start date'))
@ -222,9 +249,7 @@ class Pool(models.Model):
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.set_number()
final_invoice.save()
for line in invoice.lines.all().order_by('pk'):
@ -291,13 +316,14 @@ class Counter(models.Model):
class Invoice(AbstractInvoice):
number = models.PositiveIntegerField(default=0)
formatted_number = models.CharField(max_length=200)
def format_number(self):
return 'F-%s-%s-%06d' % (
self.regie.slug.upper(),
self.created_at.strftime('%y-%m'),
int(self.number),
def set_number(self):
self.number = Counter.get_count(
regie=self.regie,
name=self.regie.get_counter_name(self.created_at),
)
self.formatted_number = self.regie.format_number(self.created_at, self.number)
class InjectedLine(models.Model):

View File

@ -34,7 +34,7 @@
{% if pool.draft %}
{% blocktrans with number=line.invoice_id payer=line.invoice.payer amount=line.invoice.total_amount %}Invoice <a href="{{ journal_url }}?invoice_id={{ number }}">TMP-{{ number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% else %}
{% blocktrans with invoice_number=line.invoice.format_number payer=line.invoice.payer amount=line.invoice.total_amount number=line.invoice.number regie_id=line.invoice.regie_id %}Invoice <a href="{{ journal_url }}?invoice_number={{ number }}&regie={{ regie_id }}">{{ invoice_number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% blocktrans with invoice_number=line.invoice.formatted_number payer=line.invoice.payer amount=line.invoice.total_amount number=line.invoice.number regie_id=line.invoice.regie_id %}Invoice <a href="{{ journal_url }}?invoice_number={{ number }}&regie={{ regie_id }}">{{ invoice_number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% endif %}
</h3>
<ul class="objects-list" data-invoice-id="{{ line.invoice_id }}">

View File

@ -62,7 +62,7 @@
{% if pool.draft and line.invoice %}
TMP-{{ line.invoice_id }}
{% elif line.invoice %}
{{ line.invoice.format_number }}
{{ line.invoice.formatted_number }}
{% endif %}
</td>
<td>

View File

@ -31,7 +31,9 @@
<h3>{% trans "Parameters" %}</h3>
<ul>
<li>{% trans "Identifier:" %} {{ regie.slug }}</li>
<li>{% trans "Cashier role:" %} {{ regie.cashier_role }}</li>
<li>{% trans "Cashier role:" %} {{ regie.cashier_role|default:'' }}</li>
<li>{% trans "Counter name:" %} <code>{{ regie.counter_name }}</code></li>
<li>{% trans "Number format:" %} <code>{{ regie.number_format }}</code></li>
</ul>
</div>

View File

@ -107,7 +107,7 @@ regie_detail = RegieDetailView.as_view()
class RegieEditView(UpdateView):
template_name = 'lingo/invoicing/manager_regie_form.html'
model = Regie
fields = ['label', 'description', 'cashier_role']
fields = ['label', 'description', 'cashier_role', 'counter_name', 'number_format']
def get_success_url(self):
return reverse('lingo-manager-invoicing-regie-detail', args=[self.object.pk])
@ -345,7 +345,7 @@ class PoolJournalView(DetailView):
if self.object.draft:
line_model = DraftInvoiceLine
filter_model = DraftInvoiceLineFilterSet
all_lines = line_model.objects.filter(pool=self.object).order_by('pk')
all_lines = line_model.objects.filter(pool=self.object).order_by('pk').select_related('invoice')
self.object.error_count = len([line for line in all_lines if line.status == 'error'])
self.object.warning_count = len([line for line in all_lines if line.status == 'warning'])
self.object.success_count = len([line for line in all_lines if line.status == 'success'])

View File

@ -526,9 +526,9 @@ def test_detail_pool_invoices(app, admin_user, draft):
date_issue=datetime.date.today(), regie=regie, pool=pool, payer='payer:2'
)
if not draft:
invoice1.number = 42
invoice1.set_number()
invoice1.save()
invoice2.number = 43
invoice2.set_number()
invoice2.save()
line11 = line_model.objects.create(
@ -604,8 +604,9 @@ def test_detail_pool_invoices(app, admin_user, draft):
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'
).text() == 'Invoice F%02s-%s-0000001 addressed to payer:1, amount 6.00€' % (
regie.pk,
invoice1.created_at.strftime('%y-%m'),
)
assert len(resp.pyquery('ul[data-invoice-id="%s"] li' % invoice1.pk)) == 3
assert (
@ -628,8 +629,9 @@ def test_detail_pool_invoices(app, admin_user, draft):
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'
).text() == 'Invoice F%02d-%s-0000002 addressed to payer:2, amount 1.00€' % (
regie.pk,
invoice2.created_at.strftime('%y-%m'),
)
assert len(resp.pyquery('ul[data-invoice-id="%s"] li' % invoice2.pk)) == 1
assert (
@ -766,9 +768,9 @@ def test_journal_pool_lines(app, admin_user, draft):
date_issue=datetime.date.today(), regie=regie2, pool=pool, payer='payer:2'
)
if not draft:
invoice1.number = 42
invoice1.set_number()
invoice1.save()
invoice2.number = 35
invoice2.set_number()
invoice2.save()
lines = [
@ -972,9 +974,14 @@ def test_journal_pool_lines(app, admin_user, draft):
else:
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'invoice_number': invoice1.number},
params={'invoice_number': invoice1.formatted_number},
)
assert len(resp.pyquery('tr td.status')) == 1
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'invoice_number': invoice1.created_at.strftime('%y-%m')},
)
assert len(resp.pyquery('tr td.status')) == 2
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'pk': lines[0].pk},

View File

@ -1659,8 +1659,8 @@ def test_promote_pool():
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
assert Counter.objects.get(regie=regie1, name=today.strftime('%y')).value == 2
assert Counter.objects.get(regie=regie2, name=today.strftime('%y')).value == 1
test_counts()
@ -1678,7 +1678,7 @@ def test_promote_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')
assert final_invoice1.formatted_number == 'F%02d-%s-0000001' % (regie1.pk, today.strftime('%y-%m'))
final_line11 = InvoiceLine.objects.order_by('pk')[0]
assert final_line11.event_date == line11.event_date
@ -1721,7 +1721,7 @@ def test_promote_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')
assert final_invoice2.formatted_number == 'F%02d-%s-0000002' % (regie1.pk, today.strftime('%y-%m'))
final_line21 = InvoiceLine.objects.order_by('pk')[2]
assert final_line21.event_date == line21.event_date
@ -1796,7 +1796,7 @@ def test_promote_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')
assert final_invoice3.formatted_number == 'F%02d-%s-0000001' % (regie2.pk, today.strftime('%y-%m'))
with pytest.raises(PoolPromotionError) as excinfo:
old_pool.promote()

View File

@ -194,3 +194,35 @@ def test_counter():
assert counter2.value == 1
counter3 = Counter.objects.get(regie=regie2, name='bar')
assert counter3.value == 1
def test_regie_counter_name():
regie = Regie.objects.create()
assert regie.counter_name == '{yy}'
assert regie.get_counter_name(datetime.date(2023, 1, 1)) == '23'
assert regie.get_counter_name(datetime.date(2024, 1, 1)) == '24'
regie.counter_name = '{yyyy}'
regie.save()
assert regie.get_counter_name(datetime.date(2023, 1, 1)) == '2023'
assert regie.get_counter_name(datetime.date(2024, 1, 1)) == '2024'
regie.counter_name = '{yy}-{mm}'
regie.save()
assert regie.get_counter_name(datetime.date(2023, 1, 1)) == '23-01'
assert regie.get_counter_name(datetime.date(2023, 2, 1)) == '23-02'
assert regie.get_counter_name(datetime.date(2024, 12, 1)) == '24-12'
def test_regie_format_number():
regie = Regie.objects.create()
assert regie.number_format == 'F{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.format_number(datetime.date(2023, 2, 15), 42) == 'F%02d-23-02-0000042' % regie.pk
assert regie.format_number(datetime.date(2024, 12, 15), 42000000) == 'F%02d-24-12-42000000' % regie.pk
regie.number_format = 'Ffoobar-{yyyy}-{number:08d}'
regie.save()
assert regie.format_number(datetime.date(2023, 2, 15), 42) == 'Ffoobar-2023-00000042'
assert regie.format_number(datetime.date(2024, 12, 15), 42000000) == 'Ffoobar-2024-42000000'