invoicing: parametrize counter name and number format (#73744) #20
|
@ -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'),
|
||||
|
|
|
@ -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'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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
|
||||
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):
|
||||
|
|
|
@ -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 }}®ie={{ 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 }}®ie={{ 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 }}">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue
passer à 07d par défaut
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 :)