diff --git a/lingo/invoicing/management/commands/generate_invoices.py b/lingo/invoicing/management/commands/generate_invoices.py index 8d80965..6571f99 100644 --- a/lingo/invoicing/management/commands/generate_invoices.py +++ b/lingo/invoicing/management/commands/generate_invoices.py @@ -18,6 +18,7 @@ import datetime from django.core.management.base import BaseCommand, CommandError +from lingo.invoicing.models import Campaign from lingo.invoicing.utils import generate_invoices @@ -27,6 +28,8 @@ class Command(BaseCommand): def add_arguments(self, parser): 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: @@ -37,8 +40,26 @@ class Command(BaseCommand): date_end = datetime.datetime.fromisoformat(options['date_end']).date() except ValueError: raise CommandError('Bad value "%s" for date_end' % options['date_end']) + if options.get('date_issue'): + try: + date_issue = datetime.datetime.fromisoformat(options['date_issue']).date() + except ValueError: + raise CommandError('Bad value "%s" for date_issue' % options['date_issue']) + else: + date_issue = date_end - generate_invoices(date_start=date_start, date_end=date_end) + try: + campaign = Campaign.objects.get(date_start=date_start, date_end=date_end) + except Campaign.DoesNotExist: + campaigns = Campaign.objects.extra( + where=["(date_start, date_end) OVERLAPS (%s, %s)"], params=[date_start, date_end] + ) + if campaigns.exists(): + raise CommandError('Overlapping campaigns already exist') + campaign = Campaign.objects.create( + date_start=date_start, date_end=date_end, date_issue=date_issue + ) + generate_invoices(campaign=campaign, draft=options['draft']) self.stdout.write( self.style.SUCCESS('Invoicing generation OK (start: %s, end: %s)' % (date_start, date_end)) diff --git a/lingo/invoicing/migrations/0004_campaign.py b/lingo/invoicing/migrations/0004_campaign.py new file mode 100644 index 0000000..46b2565 --- /dev/null +++ b/lingo/invoicing/migrations/0004_campaign.py @@ -0,0 +1,65 @@ +# Generated by Django 2.2.26 on 2022-12-01 10:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoicing', '0003_invoice'), + ] + + operations = [ + migrations.CreateModel( + name='Campaign', + fields=[ + ( + 'id', + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ('date_start', models.DateField(verbose_name='Start date')), + ('date_end', models.DateField(verbose_name='End date')), + ('date_issue', models.DateField(verbose_name='Issue date')), + ], + ), + migrations.CreateModel( + name='Pool', + fields=[ + ( + 'id', + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ('draft', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ( + 'campaign', + models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.Campaign'), + ), + ], + ), + migrations.AddField( + model_name='draftinvoice', + name='pool', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.Pool'), + preserve_default=False, + ), + migrations.AddField( + model_name='draftinvoiceline', + name='pool', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.Pool'), + preserve_default=False, + ), + migrations.AddField( + model_name='invoice', + name='pool', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.Pool'), + preserve_default=False, + ), + migrations.AddField( + model_name='invoiceline', + name='pool', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.Pool'), + preserve_default=False, + ), + ] diff --git a/lingo/invoicing/models.py b/lingo/invoicing/models.py index 4471994..242d6be 100644 --- a/lingo/invoicing/models.py +++ b/lingo/invoicing/models.py @@ -87,6 +87,18 @@ class Regie(models.Model): return created, regie +class Campaign(models.Model): + date_start = models.DateField(_('Start date')) + date_end = models.DateField(_('End date')) + date_issue = models.DateField(_('Issue date')) + + +class Pool(models.Model): + campaign = models.ForeignKey(Campaign, on_delete=models.PROTECT) + draft = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + class AbstractInvoice(models.Model): label = models.CharField(_('Label'), max_length=300) total_amount = models.DecimalField(max_digits=9, decimal_places=2, default=0) @@ -94,6 +106,8 @@ class AbstractInvoice(models.Model): regie = models.ForeignKey(Regie, on_delete=models.PROTECT) payer = models.CharField(_('Payer'), max_length=300) + pool = models.ForeignKey(Pool, on_delete=models.PROTECT) + class Meta: abstract = True @@ -125,6 +139,8 @@ class AbstractInvoiceLine(models.Model): ], ) + pool = models.ForeignKey(Pool, on_delete=models.PROTECT) + class Meta: abstract = True diff --git a/lingo/invoicing/utils.py b/lingo/invoicing/utils.py index 7f3899e..34a3e47 100644 --- a/lingo/invoicing/utils.py +++ b/lingo/invoicing/utils.py @@ -26,14 +26,15 @@ from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine from lingo.pricing.models import AgendaPricing, AgendaPricingNotFound, PricingError -def get_agendas(date_start, date_end): +def get_agendas(pool): agendas_pricings = AgendaPricing.objects.filter(flat_fee_schedule=False).extra( - where=["(date_start, date_end) OVERLAPS (%s, %s)"], params=[date_start, date_end] + where=["(date_start, date_end) OVERLAPS (%s, %s)"], + params=[pool.campaign.date_start, pool.campaign.date_end], ) return Agenda.objects.filter(pk__in=agendas_pricings.values('agendas')).order_by('pk') -def get_all_subscriptions(agendas, date_start, date_end): +def get_all_subscriptions(agendas, pool): # result: # { # 'user_id': { @@ -44,7 +45,11 @@ def get_all_subscriptions(agendas, date_start, date_end): # } all_subscriptions = {} for agenda in agendas: - subscriptions = get_subscriptions(agenda_slug=agenda.slug, date_start=date_start, date_end=date_end) + subscriptions = get_subscriptions( + agenda_slug=agenda.slug, + date_start=pool.campaign.date_start, + date_end=pool.campaign.date_end, + ) for subscription in subscriptions: if subscription['user_external_id'] not in all_subscriptions: all_subscriptions[subscription['user_external_id']] = {} @@ -55,9 +60,7 @@ def get_all_subscriptions(agendas, date_start, date_end): return all_subscriptions -def get_invoice_lines_for_user( - agendas, agendas_pricings, user_external_id, subscriptions, date_start, date_end -): +def get_invoice_lines_for_user(agendas, agendas_pricings, user_external_id, subscriptions, pool): def get_agenda_pricing(agendas_pricings_for_agenda, date_event): # same logic as AgendaPricing.get_agenda_pricing for agenda_pricing in agendas_pricings_for_agenda: @@ -93,8 +96,8 @@ def get_invoice_lines_for_user( check_status_list = get_check_status( agenda_slugs=list(subscriptions.keys()), user_external_id=user_external_id, - date_start=date_start, - date_end=date_end, + date_start=pool.campaign.date_start, + date_end=pool.campaign.date_end, ) request = RequestFactory().get('/') # XXX agendas_by_slug = {a.slug: a for a in agendas} @@ -150,6 +153,7 @@ def get_invoice_lines_for_user( event=serialized_event, pricing_data=pricing_error, status='error', + pool=pool, ) ) else: @@ -166,16 +170,20 @@ def get_invoice_lines_for_user( event=serialized_event, pricing_data=pricing_data, status='success', + pool=pool, ) ) DraftInvoiceLine.objects.bulk_create(lines) return lines -def get_all_invoice_lines(agendas, subscriptions, date_start, date_end): +def get_all_invoice_lines(agendas, subscriptions, pool): agendas_pricings = ( AgendaPricing.objects.filter(flat_fee_schedule=False) - .extra(where=["(date_start, date_end) OVERLAPS (%s, %s)"], params=[date_start, date_end]) + .extra( + where=["(date_start, date_end) OVERLAPS (%s, %s)"], + params=[pool.campaign.date_start, pool.campaign.date_end], + ) .prefetch_related('agendas', 'pricing__criterias', 'pricing__categories') ) @@ -187,13 +195,12 @@ def get_all_invoice_lines(agendas, subscriptions, date_start, date_end): agendas_pricings=agendas_pricings, user_external_id=user_external_id, subscriptions=user_subs, - date_start=date_start, - date_end=date_end, + pool=pool, ) return lines -def generate_invoices_from_lines(agendas, all_lines, date_start, date_end): +def generate_invoices_from_lines(agendas, all_lines, pool): agendas_by_slug = {a.slug: a for a in agendas} # regroup lines by regie, and by payer_external_id (payer) @@ -222,10 +229,15 @@ def generate_invoices_from_lines(agendas, all_lines, date_start, date_end): for regie_id, regie_subs in lines_by_regie.items(): for payer_external_id, adult_lines in regie_subs.items(): invoice = DraftInvoice.objects.create( - label=_('Invoice from %s to %s') % (date_start, date_end - datetime.timedelta(days=1)), - date_issue=date_end, # XXX + label=_('Invoice from %s to %s') + % ( + pool.campaign.date_start, + pool.campaign.date_end - datetime.timedelta(days=1), + ), + date_issue=pool.campaign.date_issue, regie_id=regie_id, payer=payer_external_id, + pool=pool, ) DraftInvoiceLine.objects.filter(pk__in=[line.pk for line in adult_lines]).update(invoice=invoice) invoices.append(invoice) @@ -233,14 +245,13 @@ def generate_invoices_from_lines(agendas, all_lines, date_start, date_end): return invoices -def generate_invoices(date_start, date_end): +def generate_invoices(campaign, draft=True): + pool = campaign.pool_set.create(draft=draft) # get agendas with pricing corresponding to the period - agendas = get_agendas(date_start=date_start, date_end=date_end) + agendas = get_agendas(pool=pool) # get subscriptions for each agenda, for the period - subscriptions = get_all_subscriptions(agendas=agendas, date_start=date_start, date_end=date_end) + subscriptions = get_all_subscriptions(agendas=agendas, pool=pool) # get invoice lines for all subscribed users, for each agenda in the corresponding period - lines = get_all_invoice_lines( - agendas=agendas, subscriptions=subscriptions, date_start=date_start, date_end=date_end - ) + lines = get_all_invoice_lines(agendas=agendas, subscriptions=subscriptions, pool=pool) # and generate invoices - generate_invoices_from_lines(agendas=agendas, all_lines=lines, date_start=date_start, date_end=date_end) + generate_invoices_from_lines(agendas=agendas, all_lines=lines, pool=pool) diff --git a/tests/invoicing/test_invoice_generation.py b/tests/invoicing/test_invoice_generation.py index f6077e8..54778c6 100644 --- a/tests/invoicing/test_invoice_generation.py +++ b/tests/invoicing/test_invoice_generation.py @@ -9,7 +9,7 @@ from django.test.utils import CaptureQueriesContext from lingo.agendas.chrono import ChronoError from lingo.agendas.models import Agenda from lingo.invoicing import utils -from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, Regie +from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Pool, Regie from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingError pytestmark = pytest.mark.django_db @@ -21,12 +21,18 @@ def test_get_agendas(): agenda3 = Agenda.objects.create(label='Agenda 3') Agenda.objects.create(label='Agenda 4') pricing = Pricing.objects.create(label='Foo bar 1') + 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, + ) # no agenda pricing defined - assert ( - list(utils.get_agendas(date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1))) - == [] - ) + assert list(utils.get_agendas(pool=pool)) == [] # agenda pricing, but for flat_fee_schedule agenda_pricing = AgendaPricing.objects.create( @@ -36,10 +42,7 @@ def test_get_agendas(): flat_fee_schedule=True, # wrong config ) agenda_pricing.agendas.add(agenda1) - assert ( - list(utils.get_agendas(date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1))) - == [] - ) + assert list(utils.get_agendas(pool=pool)) == [] # create some agenda pricing agenda_pricing1 = AgendaPricing.objects.create( @@ -52,74 +55,77 @@ def test_get_agendas(): date_start=datetime.date(year=2022, month=9, day=1), date_end=datetime.date(year=2022, month=10, day=1), ) - assert ( - list(utils.get_agendas(date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1))) - == [] - ) + assert list(utils.get_agendas(pool=pool)) == [] # link agendas to agenda pricing agenda_pricing1.agendas.add(agenda1, agenda2) agenda_pricing2.agendas.add(agenda3) - assert list( - utils.get_agendas(date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1)) - ) == [agenda3] - assert list( - utils.get_agendas(date_start=datetime.date(2021, 9, 1), date_end=datetime.date(2021, 10, 1)) - ) == [agenda1, agenda2] - assert ( - list(utils.get_agendas(date_start=datetime.date(2022, 8, 31), date_end=datetime.date(2022, 9, 1))) - == [] - ) - assert list( - utils.get_agendas(date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 9, 2)) - ) == [agenda3] - assert list( - utils.get_agendas(date_start=datetime.date(2022, 9, 30), date_end=datetime.date(2022, 10, 1)) - ) == [agenda3] - assert ( - list(utils.get_agendas(date_start=datetime.date(2022, 10, 1), date_end=datetime.date(2022, 10, 2))) - == [] - ) - assert list( - utils.get_agendas(date_start=datetime.date(2021, 9, 15), date_end=datetime.date(2022, 9, 15)) - ) == [agenda1, agenda2, agenda3] + assert list(utils.get_agendas(pool=pool)) == [agenda3] + campaign.date_start = datetime.date(2021, 9, 1) + campaign.date_end = datetime.date(2021, 10, 1) + campaign.save() + assert list(utils.get_agendas(pool=pool)) == [agenda1, agenda2] + campaign.date_start = datetime.date(2022, 8, 31) + campaign.date_end = datetime.date(2022, 9, 1) + campaign.save() + assert list(utils.get_agendas(pool=pool)) == [] + campaign.date_start = datetime.date(2022, 9, 1) + campaign.date_end = datetime.date(2022, 9, 2) + campaign.save() + assert list(utils.get_agendas(pool=pool)) == [agenda3] + campaign.date_start = datetime.date(2022, 9, 30) + campaign.date_end = datetime.date(2022, 10, 1) + campaign.save() + assert list(utils.get_agendas(pool=pool)) == [agenda3] + campaign.date_start = datetime.date(2022, 10, 1) + campaign.date_end = datetime.date(2022, 10, 2) + campaign.save() + assert list(utils.get_agendas(pool=pool)) == [] + campaign.date_start = datetime.date(2021, 9, 15) + campaign.date_end = datetime.date(2022, 9, 15) + campaign.save() + assert list(utils.get_agendas(pool=pool)) == [agenda1, agenda2, agenda3] @mock.patch('lingo.invoicing.utils.get_subscriptions') def test_get_all_subscriptions_error(mock_subscriptions): agenda = Agenda.objects.create(label='Agenda') + 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, + ) mock_subscriptions.side_effect = ChronoError('foo baz') with pytest.raises(ChronoError): - utils.get_all_subscriptions( - agendas=[agenda], date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1) - ) + utils.get_all_subscriptions(agendas=[agenda], pool=pool) @mock.patch('lingo.invoicing.utils.get_subscriptions') def test_get_all_subscriptions(mock_subscriptions): agenda1 = Agenda.objects.create(label='Agenda 1') agenda2 = Agenda.objects.create(label='Agenda 2') + 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, + ) # no agendas - assert ( - utils.get_all_subscriptions( - agendas=[], date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1) - ) - == {} - ) + assert utils.get_all_subscriptions(agendas=[], pool=pool) == {} assert mock_subscriptions.call_args_list == [] # no subscriptions mock_subscriptions.return_value = [] - assert ( - utils.get_all_subscriptions( - agendas=[agenda1, agenda2], - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), - ) - == {} - ) + assert utils.get_all_subscriptions(agendas=[agenda1, agenda2], pool=pool) == {} assert mock_subscriptions.call_args_list == [ mock.call( agenda_slug='agenda-1', date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1) @@ -157,9 +163,7 @@ def test_get_all_subscriptions(mock_subscriptions): }, ], ] - assert utils.get_all_subscriptions( - agendas=[agenda1, agenda2], date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1) - ) == { + assert utils.get_all_subscriptions(agendas=[agenda1, agenda2], pool=pool) == { 'user:1': { 'agenda-1': [ { @@ -203,6 +207,15 @@ def test_get_all_subscriptions(mock_subscriptions): @mock.patch('lingo.invoicing.utils.get_check_status') def test_get_invoice_lines_for_user_check_status_error(mock_status): + 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, + ) mock_status.side_effect = ChronoError('foo baz') with pytest.raises(ChronoError): utils.get_invoice_lines_for_user( @@ -210,8 +223,7 @@ def test_get_invoice_lines_for_user_check_status_error(mock_status): agendas_pricings=[], user_external_id='user:1', subscriptions={'foo': 'bar'}, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) @@ -227,6 +239,15 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s date_end=datetime.date(year=2022, month=10, day=1), ) agenda_pricing.agendas.add(agenda1, agenda2) + 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, + ) # no agendas assert ( @@ -243,8 +264,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s }, ], }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) == [] ) @@ -258,8 +278,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s agendas_pricings=[agenda_pricing], user_external_id='user:1', subscriptions={}, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) == [] ) @@ -282,8 +301,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s }, ], }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) == [] ) @@ -323,8 +341,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s }, ], }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) == [] ) @@ -411,8 +428,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s }, ], }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) assert mock_pricing_data_event.call_args_list == [ mock.call( @@ -495,6 +511,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s } assert line1.pricing_data == {'foo1': 'bar1', 'pricing': 1} assert line1.status == 'success' + assert line1.pool == pool assert isinstance(line2, DraftInvoiceLine) assert line2.invoice is None assert line2.slug == 'agenda-1@event-2' @@ -512,6 +529,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s } assert line2.pricing_data == {'foo2': 'bar2', 'pricing': 2} assert line2.status == 'success' + assert line2.pool == pool assert isinstance(line3, DraftInvoiceLine) assert line3.invoice is None assert line3.slug == 'agenda-2@eveeent-1' @@ -529,6 +547,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s } assert line3.pricing_data == {'foo3': 'bar3', 'pricing': 3} assert line3.status == 'success' + assert line3.pool == pool assert isinstance(line4, DraftInvoiceLine) assert line4.invoice is None assert line4.slug == 'agenda-2@eveeent-2' @@ -546,6 +565,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s } assert line4.pricing_data == {'foo4': 'bar4', 'pricing': 4} assert line4.status == 'success' + assert line4.pool == pool @mock.patch('lingo.invoicing.utils.get_check_status') @@ -559,6 +579,15 @@ def test_get_invoice_lines_for_user_check_status_subscriptions_dates(mock_pricin date_end=datetime.date(year=2022, month=10, day=1), ) agenda_pricing.agendas.add(agenda) + 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, + ) mock_pricing_data_event.return_value = {'foo': 'bar', 'pricing': 42} mock_status.return_value = [ @@ -589,8 +618,7 @@ def test_get_invoice_lines_for_user_check_status_subscriptions_dates(mock_pricin }, ], }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) == [] ) @@ -611,8 +639,7 @@ def test_get_invoice_lines_for_user_check_status_subscriptions_dates(mock_pricin }, ], }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) == [] ) @@ -644,8 +671,7 @@ def test_get_invoice_lines_for_user_check_status_subscriptions_dates(mock_pricin }, ], }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) assert len(lines) == 1 assert mock_pricing_data_event.call_args_list == [ @@ -690,6 +716,15 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_statu date_end=datetime.date(year=2022, month=11, day=1), ) agenda_pricing3.agendas.add(agenda) + campaign = Campaign.objects.create( + date_start=datetime.date(2022, 1, 1), + date_end=datetime.date(2023, 1, 1), + date_issue=datetime.date(2023, 1, 31), + ) + pool = Pool.objects.create( + campaign=campaign, + draft=True, + ) pricing_data_event_patch = mock.patch.object(AgendaPricing, 'get_pricing_data_for_event', autospec=True) @@ -722,8 +757,7 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_statu }, ], }, - date_start=datetime.date(2022, 1, 1), - date_end=datetime.date(2023, 1, 1), + pool=pool, ) assert len(lines) == 1 assert mock_pricing_data_event.call_args_list[0][0][0] == agenda_pricing1 @@ -757,8 +791,7 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_statu }, ], }, - date_start=datetime.date(2022, 1, 1), - date_end=datetime.date(2023, 1, 1), + pool=pool, ) assert len(lines) == 1 assert mock_pricing_data_event.call_args_list[0][0][0] == agenda_pricing3 @@ -791,8 +824,7 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_statu }, ], }, - date_start=datetime.date(2022, 1, 1), - date_end=datetime.date(2023, 1, 1), + pool=pool, ) assert len(lines) == 0 assert mock_pricing_data_event.call_args_list == [] @@ -809,6 +841,15 @@ def test_get_invoice_lines_for_user_check_status_pricing_error(mock_pricing_data date_end=datetime.date(year=2022, month=10, day=1), ) agenda_pricing.agendas.add(agenda) + 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, + ) mock_pricing_data_event.side_effect = [ {'foo1': 'bar1', 'pricing': 1}, @@ -860,8 +901,7 @@ def test_get_invoice_lines_for_user_check_status_pricing_error(mock_pricing_data }, ], }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) assert len(lines) == 3 line1, line2, line3 = lines @@ -882,6 +922,7 @@ def test_get_invoice_lines_for_user_check_status_pricing_error(mock_pricing_data } assert line1.pricing_data == {'foo1': 'bar1', 'pricing': 1} assert line1.status == 'success' + assert line1.pool == pool assert isinstance(line2, DraftInvoiceLine) assert line2.invoice is None assert line2.slug == 'agenda@event-2' @@ -899,6 +940,7 @@ def test_get_invoice_lines_for_user_check_status_pricing_error(mock_pricing_data } assert line2.pricing_data == {'error': {}} assert line2.status == 'error' + assert line2.pool == pool assert isinstance(line3, DraftInvoiceLine) assert line3.invoice is None assert line3.slug == 'agenda@event-3' @@ -916,6 +958,7 @@ def test_get_invoice_lines_for_user_check_status_pricing_error(mock_pricing_data } assert line3.pricing_data == {'foo3': 'bar3', 'pricing': 3} assert line3.status == 'success' + assert line3.pool == pool @mock.patch('lingo.invoicing.utils.get_invoice_lines_for_user') @@ -923,21 +966,33 @@ def test_get_all_invoice_lines(mock_user_lines): agenda1 = Agenda.objects.create(label='Agenda 1') agenda2 = Agenda.objects.create(label='Agenda 2') Agenda.objects.create(label='Agenda 3') + 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, + ) line1 = DraftInvoiceLine.objects.create( quantity=0, unit_amount=0, total_amount=0, + pool=pool, ) line2 = DraftInvoiceLine.objects.create( quantity=0, unit_amount=0, total_amount=0, + pool=pool, ) line3 = DraftInvoiceLine.objects.create( quantity=0, unit_amount=0, total_amount=0, + pool=pool, ) mock_user_lines.side_effect = [[line1, line2], [line3]] @@ -947,8 +1002,7 @@ def test_get_all_invoice_lines(mock_user_lines): utils.get_all_invoice_lines( agendas=[agenda1, agenda2], subscriptions={}, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) == [] ) @@ -961,8 +1015,7 @@ def test_get_all_invoice_lines(mock_user_lines): 'user:1': {'foo': 'bar1'}, 'user:2': {'foo': 'bar2'}, }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) == [line1, line2, line3] assert mock_user_lines.call_args_list == [ mock.call( @@ -970,16 +1023,14 @@ def test_get_all_invoice_lines(mock_user_lines): agendas_pricings=mock.ANY, user_external_id='user:1', subscriptions={'foo': 'bar1'}, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ), mock.call( agendas=[agenda1, agenda2], agendas_pricings=mock.ANY, user_external_id='user:2', subscriptions={'foo': 'bar2'}, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ), ] @@ -1029,6 +1080,16 @@ def test_get_all_invoice_lines_queryset(mock_status): ) agenda_pricing22.agendas.add(agenda2) + campaign = Campaign.objects.create( + date_start=datetime.date(2022, 9, 1), + date_end=datetime.date(2022, 11, 1), + date_issue=datetime.date(2022, 11, 30), + ) + pool = Pool.objects.create( + campaign=campaign, + draft=True, + ) + mock_status.return_value = [ { 'event': { @@ -1109,8 +1170,7 @@ def test_get_all_invoice_lines_queryset(mock_status): ], }, }, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 11, 1), + pool=pool, ) assert lines assert len(ctx.captured_queries) == 7 @@ -1124,6 +1184,16 @@ def test_generate_invoices_from_lines(): agenda3 = Agenda.objects.create(label='Agenda 3', regie=regie1) agenda4 = Agenda.objects.create(label='Agenda 4') # regie not configured + 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, + ) + line_error = DraftInvoiceLine.objects.create( event={'agenda': 'agenda-1'}, quantity=0, @@ -1132,6 +1202,7 @@ def test_generate_invoices_from_lines(): user_external_id='user:1', payer_external_id='user:1', status='error', + pool=pool, ) line1 = DraftInvoiceLine.objects.create( event={'agenda': 'agenda-1'}, @@ -1141,6 +1212,7 @@ def test_generate_invoices_from_lines(): user_external_id='user:1', payer_external_id='user:1', status='success', + pool=pool, ) line2 = DraftInvoiceLine.objects.create( event={'agenda': 'agenda-1'}, @@ -1150,6 +1222,7 @@ def test_generate_invoices_from_lines(): user_external_id='user:1', payer_external_id='user:1', status='success', + pool=pool, ) line3 = DraftInvoiceLine.objects.create( event={'agenda': 'agenda-2'}, @@ -1159,6 +1232,7 @@ def test_generate_invoices_from_lines(): user_external_id='user:1', payer_external_id='user:1', status='success', + pool=pool, ) line4 = DraftInvoiceLine.objects.create( event={'agenda': 'agenda-2'}, @@ -1168,6 +1242,7 @@ def test_generate_invoices_from_lines(): user_external_id='user:1', payer_external_id='user:2', status='success', + pool=pool, ) line5 = DraftInvoiceLine.objects.create( event={'agenda': 'agenda-3'}, @@ -1177,6 +1252,7 @@ def test_generate_invoices_from_lines(): user_external_id='user:2', payer_external_id='user:1', status='success', + pool=pool, ) DraftInvoiceLine.objects.create( # not used for generation event={'agenda': 'agenda-3'}, @@ -1186,6 +1262,7 @@ def test_generate_invoices_from_lines(): user_external_id='user:2', payer_external_id='user:1', status='success', + pool=pool, ) line6 = DraftInvoiceLine.objects.create( event={'agenda': 'agenda-4'}, # regie not configured @@ -1195,21 +1272,20 @@ def test_generate_invoices_from_lines(): user_external_id='user:1', payer_external_id='user:1', status='success', + pool=pool, ) invoices = utils.generate_invoices_from_lines( agendas=[], all_lines=[line_error, line1, line2, line3, line4, line5, line6], - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) assert len(invoices) == 0 invoices = utils.generate_invoices_from_lines( agendas=[agenda1, agenda2, agenda3, agenda4], all_lines=[line_error, line1, line2, line3, line4, line5, line6], - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) assert len(invoices) == 3 invoice1, invoice2, invoice3 = invoices @@ -1220,23 +1296,26 @@ def test_generate_invoices_from_lines(): assert isinstance(invoice1, DraftInvoice) assert invoice1.label == 'Invoice from 2022-09-01 to 2022-09-30' assert invoice1.total_amount == 8 - assert invoice1.date_issue == datetime.date(2022, 10, 1) + assert invoice1.date_issue == datetime.date(2022, 10, 31) assert invoice1.regie == regie1 assert invoice1.payer == 'user:1' + assert invoice1.pool == pool assert list(invoice1.lines.order_by('pk')) == [line1, line2, line5] assert isinstance(invoice2, DraftInvoice) assert invoice2.label == 'Invoice from 2022-09-01 to 2022-09-30' assert invoice2.total_amount == 3 - assert invoice2.date_issue == datetime.date(2022, 10, 1) + assert invoice2.date_issue == datetime.date(2022, 10, 31) assert invoice2.regie == regie2 assert invoice2.payer == 'user:1' + assert invoice2.pool == pool assert list(invoice2.lines.order_by('pk')) == [line3] assert isinstance(invoice3, DraftInvoice) assert invoice3.label == 'Invoice from 2022-09-01 to 2022-09-30' assert invoice3.total_amount == 4 - assert invoice3.date_issue == datetime.date(2022, 10, 1) + assert invoice3.date_issue == datetime.date(2022, 10, 31) assert invoice3.regie == regie2 assert invoice3.payer == 'user:2' + assert invoice3.pool == pool assert list(invoice3.lines.order_by('pk')) == [line4] @@ -1248,73 +1327,40 @@ def test_generate_invoices(mock_generate, mock_lines, mock_subscriptions, mock_a agenda1 = Agenda.objects.create(label='Agenda 1') agenda2 = Agenda.objects.create(label='Agenda 2') Agenda.objects.create(label='Agenda 3') - line1 = DraftInvoiceLine.objects.create( - event={'agenda': 'agenda-1'}, - quantity=1, - unit_amount=1, - total_amount=1, - user_external_id='user:1', - payer_external_id='user:1', - status='success', - ) - line2 = DraftInvoiceLine.objects.create( - event={'agenda': 'agenda-1'}, - quantity=1, - unit_amount=2, - total_amount=2, - user_external_id='user:1', - payer_external_id='user:1', - status='success', - ) - line3 = DraftInvoiceLine.objects.create( - event={'agenda': 'agenda-2'}, - quantity=1, - unit_amount=3, - total_amount=3, - user_external_id='user:1', - payer_external_id='user:1', - status='success', - ) - DraftInvoiceLine.objects.create( - event={'agenda': 'agenda-1'}, - quantity=1, - unit_amount=5, - total_amount=5, - user_external_id='user:2', - payer_external_id='user:1', - status='success', + 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), ) mock_agendas.return_value = [agenda1, agenda2] mock_subscriptions.return_value = {'foo': 'bar'} - mock_lines.return_value = [line1, line2, line3] + mock_lines.return_value = ['foo', 'bar'] # check only calls between functions - utils.generate_invoices(date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1)) - assert mock_agendas.call_args_list == [ - mock.call(date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1)) - ] + utils.generate_invoices(campaign=campaign, draft=True) + 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_subscriptions.call_args_list == [ mock.call( agendas=[agenda1, agenda2], - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) ] assert mock_lines.call_args_list == [ mock.call( agendas=[agenda1, agenda2], subscriptions={'foo': 'bar'}, - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + pool=pool, ) ] assert mock_generate.call_args_list == [ mock.call( agendas=[agenda1, agenda2], - all_lines=[line1, line2, line3], - date_start=datetime.date(2022, 9, 1), - date_end=datetime.date(2022, 10, 1), + all_lines=['foo', 'bar'], + pool=pool, ) ] @@ -1338,7 +1384,54 @@ def test_generate_invoices_cmd(): assert '%s' % excinfo.value == 'Bad value "bad-value" for date_end' assert mock_generate.call_args_list == [] + with pytest.raises(CommandError) as excinfo: + call_command('generate_invoices', '2022-09-01', '2022-10-01', '--date-issue=bad-value') + assert '%s' % excinfo.value == 'Bad value "bad-value" for date_issue' + assert mock_generate.call_args_list == [] + + assert Campaign.objects.count() == 0 call_command('generate_invoices', '2022-09-01', '2022-10-01') - assert mock_generate.call_args_list == [ - mock.call(date_start=datetime.date(2022, 9, 1), date_end=datetime.date(2022, 10, 1)) - ] + assert Campaign.objects.count() == 1 + campaign = Campaign.objects.latest('pk') + 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=campaign, draft=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=campaign, draft=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=campaign, draft=True)] + mock_generate.reset_mock() + + # with overlapping + with pytest.raises(CommandError) as excinfo: + call_command('generate_invoices', '2022-08-31', '2022-09-02') + assert '%s' % excinfo.value == 'Overlapping campaigns already exist' + assert mock_generate.call_args_list == [] + with pytest.raises(CommandError) as excinfo: + call_command('generate_invoices', '2022-09-30', '2022-10-02') + assert '%s' % excinfo.value == 'Overlapping campaigns already exist' + assert mock_generate.call_args_list == [] + + # no overlapping + 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=campaign, draft=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) + mock_generate.reset_mock() + 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=campaign, draft=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 diff --git a/tests/invoicing/test_models.py b/tests/invoicing/test_models.py index 27c87fa..4df6d4c 100644 --- a/tests/invoicing/test_models.py +++ b/tests/invoicing/test_models.py @@ -2,7 +2,7 @@ import datetime import pytest -from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine, Regie +from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine, Pool, Regie pytestmark = pytest.mark.django_db @@ -13,9 +13,19 @@ def test_invoice_total_amount(draft): invoice_model = DraftInvoice if draft else Invoice line_model = DraftInvoiceLine if draft else InvoiceLine - invoice = invoice_model.objects.create(date_issue=datetime.date.today(), regie=regie) + 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=draft, + ) + + invoice = invoice_model.objects.create(date_issue=datetime.date.today(), regie=regie, pool=pool) assert invoice.total_amount == 0 - invoice2 = invoice_model.objects.create(date_issue=datetime.date.today(), regie=regie) + invoice2 = invoice_model.objects.create(date_issue=datetime.date.today(), regie=regie, pool=pool) assert invoice2.total_amount == 0 # line with error status, ignored @@ -25,6 +35,7 @@ def test_invoice_total_amount(draft): unit_amount=0, total_amount=0, status='error', + pool=pool, ) invoice.refresh_from_db() assert invoice.total_amount == 0 @@ -73,6 +84,7 @@ def test_invoice_total_amount(draft): unit_amount=20, total_amount=20, status='success', + pool=pool, ) invoice.refresh_from_db() assert invoice.total_amount == 32 @@ -100,6 +112,7 @@ def test_invoice_total_amount(draft): unit_amount=20, total_amount=20, status='success', + pool=pool, ) invoice.refresh_from_db() assert invoice.total_amount == 12