invoicing: reduce queysets (#71528)
gitea-wip/lingo/pipeline/pr-main This commit looks good Details

This commit is contained in:
Lauréline Guérin 2022-12-01 09:56:43 +01:00
parent b995170048
commit b8f7c98661
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
3 changed files with 189 additions and 9 deletions

View File

@ -14,6 +14,7 @@
# 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
@ -54,7 +55,19 @@ def get_all_subscriptions(agendas, date_start, date_end):
return all_subscriptions
def get_invoice_lines_for_user(agendas, user_external_id, subscriptions, date_start, date_end):
def get_invoice_lines_for_user(
agendas, agendas_pricings, user_external_id, subscriptions, date_start, date_end
):
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:
@ -69,6 +82,13 @@ def get_invoice_lines_for_user(agendas, user_external_id, subscriptions, date_st
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()),
@ -98,9 +118,7 @@ def get_invoice_lines_for_user(agendas, user_external_id, subscriptions, date_st
agenda = agendas_by_slug[serialized_event['agenda']]
try:
agenda_pricing = AgendaPricing.get_agenda_pricing(
agenda=agenda, start_date=event_date, flat_fee_schedule=False
) # XXX optimize SQL request
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,
@ -155,11 +173,18 @@ def get_invoice_lines_for_user(agendas, user_external_id, subscriptions, date_st
def get_all_invoice_lines(agendas, subscriptions, date_start, date_end):
agendas_pricings = (
AgendaPricing.objects.filter(flat_fee_schedule=False)
.extra(where=["(date_start, date_end) OVERLAPS (%s, %s)"], params=[date_start, 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,
date_start=date_start,

View File

@ -507,12 +507,16 @@ class AgendaPricing(models.Model):
def compute_pricing(self, context):
criterias = {}
categories = []
# for each category (ordered)
for category in self.pricing.categories.all().order_by('pricingcriteriacategory__order'):
# for each category
for category in self.pricing.categories.all():
criterias[category.slug] = None
categories.append(category.slug)
# find the first matching criteria (criterias are ordered)
for criteria in self.pricing.criterias.filter(category=category, default=False):
for criteria in self.pricing.criterias.all():
if criteria.category_id != category.pk:
continue
if criteria.default:
continue
condition = criteria.compute_condition(context)
if condition:
criterias[category.slug] = criteria.slug
@ -520,7 +524,9 @@ class AgendaPricing(models.Model):
if criterias[category.slug] is not None:
continue
# if no match, take default criteria if only once defined
default_criterias = self.pricing.criterias.filter(category=category, default=True)
default_criterias = [
c for c in self.pricing.criterias.all() if c.default and c.category_id == category.pk
]
if len(default_criterias) > 1:
raise MultipleDefaultCriteriaCondition(details={'category': category.slug})
if not default_criterias:

View File

@ -3,12 +3,14 @@ from unittest import mock
import pytest
from django.core.management import CommandError, call_command
from django.db import connection
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.pricing.models import AgendaPricing, Pricing, PricingError
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingError
pytestmark = pytest.mark.django_db
@ -205,6 +207,7 @@ def test_get_invoice_lines_for_user_check_status_error(mock_status):
with pytest.raises(ChronoError):
utils.get_invoice_lines_for_user(
agendas=['foo'],
agendas_pricings=[],
user_external_id='user:1',
subscriptions={'foo': 'bar'},
date_start=datetime.date(2022, 9, 1),
@ -229,6 +232,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s
assert (
utils.get_invoice_lines_for_user(
agendas=[],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={
'agenda-1': [
@ -251,6 +255,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s
assert (
utils.get_invoice_lines_for_user(
agendas=[agenda1, agenda2],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={},
date_start=datetime.date(2022, 9, 1),
@ -266,6 +271,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s
assert (
utils.get_invoice_lines_for_user(
agendas=[agenda1, agenda2],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={
'agenda-1': [
@ -306,6 +312,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s
assert (
utils.get_invoice_lines_for_user(
agendas=[agenda1, agenda2],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={
'unknown': [
@ -385,6 +392,7 @@ def test_get_invoice_lines_for_user_check_status(mock_pricing_data_event, mock_s
]
lines = utils.get_invoice_lines_for_user(
agendas=[agenda1, agenda2],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={
# matching dates
@ -569,6 +577,7 @@ def test_get_invoice_lines_for_user_check_status_subscriptions_dates(mock_pricin
assert (
utils.get_invoice_lines_for_user(
agendas=[agenda],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={
'agenda': [
@ -590,6 +599,7 @@ def test_get_invoice_lines_for_user_check_status_subscriptions_dates(mock_pricin
assert (
utils.get_invoice_lines_for_user(
agendas=[agenda],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={
'agenda': [
@ -610,6 +620,7 @@ def test_get_invoice_lines_for_user_check_status_subscriptions_dates(mock_pricin
lines = utils.get_invoice_lines_for_user(
agendas=[agenda],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={
'agenda': [
@ -700,6 +711,7 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_statu
mock_pricing_data_event.return_value = {'foo': 'bar', 'pricing': 42}
lines = utils.get_invoice_lines_for_user(
agendas=[agenda],
agendas_pricings=AgendaPricing.objects.all(),
user_external_id='user:1',
subscriptions={
'agenda': [
@ -734,6 +746,7 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_statu
mock_pricing_data_event.return_value = {'foo': 'bar', 'pricing': 42}
lines = utils.get_invoice_lines_for_user(
agendas=[agenda],
agendas_pricings=AgendaPricing.objects.all(),
user_external_id='user:1',
subscriptions={
'agenda': [
@ -767,6 +780,7 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_statu
with pricing_data_event_patch as mock_pricing_data_event:
lines = utils.get_invoice_lines_for_user(
agendas=[agenda],
agendas_pricings=AgendaPricing.objects.all(),
user_external_id='user:1',
subscriptions={
'agenda': [
@ -835,6 +849,7 @@ def test_get_invoice_lines_for_user_check_status_pricing_error(mock_pricing_data
]
lines = utils.get_invoice_lines_for_user(
agendas=[agenda],
agendas_pricings=[agenda_pricing],
user_external_id='user:1',
subscriptions={
'agenda': [
@ -952,6 +967,7 @@ def test_get_all_invoice_lines(mock_user_lines):
assert mock_user_lines.call_args_list == [
mock.call(
agendas=[agenda1, agenda2],
agendas_pricings=mock.ANY,
user_external_id='user:1',
subscriptions={'foo': 'bar1'},
date_start=datetime.date(2022, 9, 1),
@ -959,6 +975,7 @@ def test_get_all_invoice_lines(mock_user_lines):
),
mock.call(
agendas=[agenda1, agenda2],
agendas_pricings=mock.ANY,
user_external_id='user:2',
subscriptions={'foo': 'bar2'},
date_start=datetime.date(2022, 9, 1),
@ -967,6 +984,138 @@ def test_get_all_invoice_lines(mock_user_lines):
]
@mock.patch('lingo.invoicing.utils.get_check_status')
def test_get_all_invoice_lines_queryset(mock_status):
# don't mock get_pricing_data_for_event to check all querysets
category1 = CriteriaCategory.objects.create(label='Foo1', slug='foo1')
criteria1 = Criteria.objects.create(label='Bar1', slug='bar1', condition='True', category=category1)
category2 = CriteriaCategory.objects.create(label='Foo2', slug='foo2')
criteria2 = Criteria.objects.create(label='Bar2', slug='bar2', condition='True', category=category2)
pricing1 = Pricing.objects.create(label='Foo bar 1')
pricing1.criterias.add(criteria1, criteria2)
pricing1.categories.add(category1, through_defaults={'order': 1})
pricing1.categories.add(category2, through_defaults={'order': 2})
pricing2 = Pricing.objects.create(label='Foo bar 2')
pricing2.criterias.add(criteria1, criteria2)
pricing2.categories.add(category1, through_defaults={'order': 1})
pricing2.categories.add(category2, through_defaults={'order': 2})
agenda1 = Agenda.objects.create(label='Agenda 1')
agenda_pricing11 = AgendaPricing.objects.create(
pricing=pricing1,
date_start=datetime.date(year=2022, month=9, day=1),
date_end=datetime.date(year=2022, month=10, day=1),
)
agenda_pricing11.agendas.add(agenda1)
agenda_pricing12 = AgendaPricing.objects.create(
pricing=pricing1,
date_start=datetime.date(year=2022, month=10, day=1),
date_end=datetime.date(year=2022, month=11, day=1),
)
agenda_pricing12.agendas.add(agenda1)
agenda2 = Agenda.objects.create(label='Agenda 2')
agenda_pricing21 = AgendaPricing.objects.create(
pricing=pricing2,
date_start=datetime.date(year=2022, month=9, day=1),
date_end=datetime.date(year=2022, month=10, day=1),
)
agenda_pricing21.agendas.add(agenda2)
agenda_pricing22 = AgendaPricing.objects.create(
pricing=pricing2,
date_start=datetime.date(year=2022, month=10, day=1),
date_end=datetime.date(year=2022, month=11, day=1),
)
agenda_pricing22.agendas.add(agenda2)
mock_status.return_value = [
{
'event': {
'agenda': 'agenda-1',
'start_datetime': '2022-09-01T12:00:00+02:00',
'slug': 'event',
'label': 'Event',
},
'check_status': {'foo': 'bar'},
'booking': {'foo': 'baz'},
},
{
'event': {
'agenda': 'agenda-1',
'start_datetime': '2022-10-01T12:00:00+02:00',
'slug': 'event',
'label': 'Event',
},
'check_status': {'foo': 'bar'},
'booking': {'foo': 'baz'},
},
{
'event': {
'agenda': 'agenda-2',
'start_datetime': '2022-09-01T12:00:00+02:00',
'slug': 'event',
'label': 'Event',
},
'check_status': {'foo': 'bar'},
'booking': {'foo': 'baz'},
},
{
'event': {
'agenda': 'agenda-2',
'start_datetime': '2022-10-01T12:00:00+02:00',
'slug': 'event',
'label': 'Event',
},
'check_status': {'foo': 'bar'},
'booking': {'foo': 'baz'},
},
]
with CaptureQueriesContext(connection) as ctx:
lines = utils.get_all_invoice_lines(
agendas=[agenda1, agenda2],
subscriptions={
'user:1': {
'agenda-1': [
{
'user_external_id': 'user:1',
'date_start': '2022-09-01',
'date_end': '2022-11-01',
},
],
'agenda-2': [
{
'user_external_id': 'user:1',
'date_start': '2022-09-01',
'date_end': '2022-11-01',
},
],
},
'user:2': {
'agenda-1': [
{
'user_external_id': 'user:2',
'date_start': '2022-09-01',
'date_end': '2022-11-01',
},
],
'agenda-2': [
{
'user_external_id': 'user:2',
'date_start': '2022-09-01',
'date_end': '2022-11-01',
},
],
},
},
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 11, 1),
)
assert lines
assert len(ctx.captured_queries) == 7
def test_generate_invoices_from_lines():
regie1 = Regie.objects.create(label='Regie 1')
regie2 = Regie.objects.create(label='Regie 2')