# lingo - payment and billing system # Copyright (C) 2022 Entr'ouvert # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import collections import datetime from django.db import transaction from django.test.client import RequestFactory from django.utils.translation import gettext_lazy as _ from lingo.agendas.chrono import get_check_status, get_subscriptions from lingo.agendas.models import Agenda from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, InjectedLine, Regie from lingo.pricing.models import AgendaPricing, AgendaPricingNotFound, PricingError def get_agendas(pool): agendas_pricings = AgendaPricing.objects.filter( flat_fee_schedule=False, agendas__regie=pool.campaign.regie, agendas__in=pool.campaign.agendas.all() ).extra( 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_users_from_subscriptions(agendas, pool): users = {} for agenda in agendas: subscriptions = get_subscriptions( agenda_slug=agenda.slug, date_start=pool.campaign.date_start, date_end=pool.campaign.date_end, ) for subscription in subscriptions: user_external_id = subscription['user_external_id'] if user_external_id in users: continue users[user_external_id] = (subscription['user_first_name'], subscription['user_last_name']) return list(users.items()) def get_invoice_lines_for_user( agendas, agendas_pricings, user_external_id, user_first_name, user_last_name, pool, payer_data_cache, ): 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: if agenda_pricing.date_start > date_event: continue if agenda_pricing.date_end <= date_event: continue return agenda_pricing raise AgendaPricingNotFound def get_cached_payer_data(request, payer_external_id, agenda_pricing=None, payer_data=None): if payer_external_id not in payer_data_cache: if agenda_pricing: # will raise a PricingError if payer_data can not be computed payer_data_cache[payer_external_id] = agenda_pricing.get_payer_data( request, payer_external_id ) elif payer_data: payer_data_cache[payer_external_id] = payer_data return payer_data_cache.get(payer_external_id) or {} if not agendas: return [] agendas_by_slug = {a.slug: a for a in agendas} agendas_pricings_by_agendas = collections.defaultdict(list) for agenda_pricing in agendas_pricings: if agenda_pricing.flat_fee_schedule: continue for agenda in agenda_pricing.agendas.all(): agendas_pricings_by_agendas[agenda.slug].append(agenda_pricing) # get check status for user_external_id, on agendas, for the period check_status_list = get_check_status( agenda_slugs=list(agendas_by_slug.keys()), user_external_id=user_external_id, date_start=pool.campaign.date_start, date_end=pool.campaign.date_end, ) request = RequestFactory().get('/') # XXX # build lines from check status lines = [] for check_status in check_status_list: serialized_event = check_status['event'] event_date = datetime.datetime.fromisoformat(serialized_event['start_datetime']).date() event_slug = '%s@%s' % (serialized_event['agenda'], serialized_event['slug']) agenda = agendas_by_slug[serialized_event['agenda']] payer_external_id = _('unknown') payer_data = {} try: agenda_pricing = get_agenda_pricing(agendas_pricings_by_agendas.get(agenda.slug), event_date) payer_external_id = agenda_pricing.get_payer_external_id(request, user_external_id) payer_data = get_cached_payer_data(request, payer_external_id, agenda_pricing=agenda_pricing) pricing_data = agenda_pricing.get_pricing_data_for_event( request=request, agenda=agenda, event=serialized_event, check_status=check_status['check_status'], user_external_id=user_external_id, payer_external_id=payer_external_id, ) except PricingError as e: # AgendaPricingNotFound: can happen if pricing model defined only on a part of the requested period pricing_error = { 'error': type(e).__name__, 'error_details': e.details, } lines.append( DraftInvoiceLine( event_date=event_date, slug=event_slug, label=serialized_event['label'], quantity=0, unit_amount=0, total_amount=0, user_external_id=user_external_id, user_first_name=user_first_name, user_last_name=user_last_name, payer_external_id=payer_external_id, payer_first_name=payer_data.get('payer_first_name') or '', payer_last_name=payer_data.get('payer_last_name') or '', payer_demat=payer_data.get('payer_demat') or False, payer_direct_debit=payer_data.get('payer_direct_debit') or False, event=serialized_event, pricing_data=pricing_error, status='warning' if isinstance(e, AgendaPricingNotFound) else 'error', pool=pool, ) ) else: # XXX log all context ! lines.append( DraftInvoiceLine( event_date=event_date, slug=event_slug, label=serialized_event['label'], quantity=1, unit_amount=pricing_data['pricing'], total_amount=pricing_data['pricing'], user_external_id=user_external_id, user_first_name=user_first_name, user_last_name=user_last_name, payer_external_id=payer_external_id, payer_first_name=payer_data['payer_first_name'], payer_last_name=payer_data['payer_last_name'], payer_demat=payer_data['payer_demat'], payer_direct_debit=payer_data['payer_direct_debit'], event=serialized_event, pricing_data=pricing_data, status='success', pool=pool, ) ) if pool.campaign.injected_lines != 'no': # fetch injected lines injected_lines = ( InjectedLine.objects.filter( regie=pool.campaign.regie, event_date__lt=pool.campaign.date_end, user_external_id=user_external_id, ) .exclude( # exclude already invoiced lines invoiceline__isnull=False ) .exclude( # exclude lines used in another campaign pk__in=DraftInvoiceLine.objects.filter(from_injected_line__isnull=False) .exclude(pool__campaign=pool.campaign) .values('from_injected_line') ) .order_by('event_date') ) if pool.campaign.injected_lines == 'period': injected_lines = injected_lines.filter( event_date__gte=pool.campaign.date_start, ) for injected_line in injected_lines: payer_external_id = injected_line.payer_external_id payer_data = { 'payer_first_name': injected_line.payer_first_name, 'payer_last_name': injected_line.payer_last_name, 'payer_demat': injected_line.payer_demat, 'payer_direct_debit': injected_line.payer_direct_debit, } payer_data = get_cached_payer_data(request, payer_external_id, payer_data=payer_data) lines.append( DraftInvoiceLine( event_date=injected_line.event_date, slug=injected_line.slug, label=injected_line.label, quantity=injected_line.quantity, unit_amount=injected_line.unit_amount, total_amount=injected_line.total_amount, user_external_id=user_external_id, user_first_name=user_first_name, user_last_name=user_last_name, payer_external_id=payer_external_id, payer_first_name=payer_data['payer_first_name'], payer_last_name=payer_data['payer_last_name'], payer_demat=payer_data['payer_demat'], payer_direct_debit=payer_data['payer_direct_debit'], status='success', pool=pool, from_injected_line=injected_line, ) ) DraftInvoiceLine.objects.bulk_create(lines) return lines def get_all_invoice_lines(agendas, users, pool): agendas_pricings = ( AgendaPricing.objects.filter(flat_fee_schedule=False) .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') ) lines = [] payer_data_cache = {} for user_external_id, (user_first_name, user_last_name) in users: # generate lines for each user lines += get_invoice_lines_for_user( agendas=agendas, agendas_pricings=agendas_pricings, user_external_id=user_external_id, user_first_name=user_first_name, user_last_name=user_last_name, pool=pool, payer_data_cache=payer_data_cache, ) return lines def generate_invoices_from_lines(all_lines, pool): regie = pool.campaign.regie # regroup lines by payer_external_id (payer) lines = {} for line in all_lines: if line.status != 'success': # ignore lines in error continue if line.payer_external_id not in lines: lines[line.payer_external_id] = { 'payer_first_name': line.payer_first_name, 'payer_last_name': line.payer_last_name, 'payer_demat': line.payer_demat, 'payer_direct_debit': line.payer_direct_debit, 'lines': [], } lines[line.payer_external_id]['lines'].append(line) # generate invoices by regie and by payer_external_id (payer) invoices = [] for payer_external_id, payer_data in lines.items(): invoice = DraftInvoice.objects.create( label=_('Invoice from %(start)s to %(end)s') % { 'start': pool.campaign.date_start, 'end': pool.campaign.date_end - datetime.timedelta(days=1), }, date_publication=pool.campaign.date_publication, date_payment_deadline=pool.campaign.date_payment_deadline, date_issue=pool.campaign.date_issue, date_debit=pool.campaign.date_debit if payer_data['payer_direct_debit'] else None, regie=regie, payer_external_id=payer_external_id, payer_first_name=payer_data['payer_first_name'], payer_last_name=payer_data['payer_last_name'], payer_demat=payer_data['payer_demat'], payer_direct_debit=payer_data['payer_direct_debit'], pool=pool, ) DraftInvoiceLine.objects.filter(pk__in=[line.pk for line in payer_data['lines']]).update( invoice=invoice ) invoices.append(invoice) return invoices def export_site( regies=True, ): '''Dump site objects to JSON-dumpable dictionnary''' data = {} if regies: data['regies'] = [x.export_json() for x in Regie.objects.all()] return data def import_site(data): results = { key: collections.defaultdict(list) for key in [ 'regies', ] } with transaction.atomic(): for cls, key in ((Regie, 'regies'),): objs = data.get(key, []) for obj in objs: created, obj = cls.import_json(obj) results[key]['all'].append(obj) if created: results[key]['created'].append(obj) else: results[key]['updated'].append(obj) return results