lingo/lingo/invoicing/utils.py

345 lines
13 KiB
Python

# 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 <http://www.gnu.org/licenses/>.
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