258 lines
10 KiB
Python
258 lines
10 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.test.client import RequestFactory
|
|
from django.utils.translation import ugettext_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
|
|
from lingo.pricing.models import AgendaPricing, AgendaPricingNotFound, PricingError
|
|
|
|
|
|
def get_agendas(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],
|
|
)
|
|
return Agenda.objects.filter(pk__in=agendas_pricings.values('agendas')).order_by('pk')
|
|
|
|
|
|
def get_all_subscriptions(agendas, pool):
|
|
# result:
|
|
# {
|
|
# 'user_id': {
|
|
# 'agenda_slug': [sub1, sub2, ...],
|
|
# 'agenda_slug2': [sub1, sub2, ...],
|
|
# },
|
|
# ...
|
|
# }
|
|
all_subscriptions = {}
|
|
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:
|
|
if subscription['user_external_id'] not in all_subscriptions:
|
|
all_subscriptions[subscription['user_external_id']] = {}
|
|
user_subs = all_subscriptions[subscription['user_external_id']]
|
|
if agenda.slug not in user_subs:
|
|
user_subs[agenda.slug] = []
|
|
user_subs[agenda.slug].append(subscription)
|
|
return all_subscriptions
|
|
|
|
|
|
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:
|
|
if agenda_pricing.date_start > date_event:
|
|
continue
|
|
if agenda_pricing.date_end <= date_event:
|
|
continue
|
|
return agenda_pricing
|
|
raise AgendaPricingNotFound
|
|
|
|
def get_subscription(subscriptions, event_date):
|
|
# get subscription matching event_date
|
|
for subscription in subscriptions:
|
|
sub_start_date = datetime.date.fromisoformat(subscription['date_start'])
|
|
sub_end_date = datetime.date.fromisoformat(subscription['date_end'])
|
|
if sub_start_date > event_date:
|
|
continue
|
|
if sub_end_date <= event_date:
|
|
continue
|
|
return subscription
|
|
|
|
if not agendas or not subscriptions:
|
|
return []
|
|
|
|
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(subscriptions.keys()),
|
|
user_external_id=user_external_id,
|
|
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}
|
|
|
|
# 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'])
|
|
if serialized_event['agenda'] not in subscriptions:
|
|
# should not happen, check-status endpoint is based on real subscriptions
|
|
continue
|
|
serialized_subscription = get_subscription(
|
|
subscriptions=subscriptions[serialized_event['agenda']],
|
|
event_date=event_date,
|
|
)
|
|
if not serialized_subscription:
|
|
# should not happen, check-status endpoint is based on real subscriptions
|
|
continue
|
|
|
|
agenda = agendas_by_slug[serialized_event['agenda']]
|
|
try:
|
|
agenda_pricing = get_agenda_pricing(agendas_pricings_by_agendas.get(agenda.slug), event_date)
|
|
pricing_data = agenda_pricing.get_pricing_data_for_event(
|
|
request=request,
|
|
agenda=agenda,
|
|
event=serialized_event,
|
|
subscription=serialized_subscription,
|
|
check_status=check_status['check_status'],
|
|
booking=check_status['booking'],
|
|
user_external_id=user_external_id,
|
|
adult_external_id=user_external_id, # XXX
|
|
)
|
|
except AgendaPricingNotFound:
|
|
# no pricing, no invoice
|
|
# can happen if pricing model defined only on a part of the requested period
|
|
# XXX error, warning, or ignore ?
|
|
continue
|
|
except PricingError as e:
|
|
# XXX explicit each error
|
|
# XXX and log context ?
|
|
pricing_error = {'error': e.details}
|
|
lines.append(
|
|
DraftInvoiceLine(
|
|
slug=event_slug,
|
|
label=serialized_event['label'],
|
|
quantity=0,
|
|
unit_amount=0,
|
|
total_amount=0,
|
|
user_external_id=user_external_id,
|
|
payer_external_id=user_external_id, # XXX
|
|
event=serialized_event,
|
|
pricing_data=pricing_error,
|
|
status='error',
|
|
pool=pool,
|
|
)
|
|
)
|
|
else:
|
|
# XXX log all context !
|
|
lines.append(
|
|
DraftInvoiceLine(
|
|
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,
|
|
payer_external_id=user_external_id, # XXX
|
|
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, 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 = []
|
|
for user_external_id, user_subs in subscriptions.items():
|
|
# generate lines for each user
|
|
lines += get_invoice_lines_for_user(
|
|
agendas=agendas,
|
|
agendas_pricings=agendas_pricings,
|
|
user_external_id=user_external_id,
|
|
subscriptions=user_subs,
|
|
pool=pool,
|
|
)
|
|
return lines
|
|
|
|
|
|
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)
|
|
lines_by_regie = {}
|
|
for line in all_lines:
|
|
if line.status != 'success':
|
|
# ignore lines in error
|
|
continue
|
|
agenda_slug = line.event['agenda']
|
|
if agenda_slug not in agendas_by_slug:
|
|
# should not happen
|
|
continue
|
|
regie = agendas_by_slug[agenda_slug].regie
|
|
if not regie:
|
|
# XXX what should we do if regie is not configured ?
|
|
continue
|
|
if regie.pk not in lines_by_regie:
|
|
lines_by_regie[regie.pk] = {}
|
|
regie_subs = lines_by_regie[regie.pk]
|
|
if line.payer_external_id not in regie_subs:
|
|
regie_subs[line.payer_external_id] = []
|
|
regie_subs[line.payer_external_id].append(line)
|
|
|
|
# generate invoices by regie and by payer_external_id (payer)
|
|
invoices = []
|
|
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')
|
|
% (
|
|
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)
|
|
|
|
return invoices
|
|
|
|
|
|
def generate_invoices(campaign, draft=True):
|
|
pool = campaign.pool_set.create(draft=draft)
|
|
# get agendas with pricing corresponding to the period
|
|
agendas = get_agendas(pool=pool)
|
|
# get subscriptions for each agenda, for the period
|
|
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, pool=pool)
|
|
# and generate invoices
|
|
generate_invoices_from_lines(agendas=agendas, all_lines=lines, pool=pool)
|