Bank transfer: Match orders based on invoice number (#2867)

This commit is contained in:
Raphael Michel
2022-10-26 11:06:45 +02:00
committed by GitHub
parent 37683781d0
commit 7c5fac306a
2 changed files with 99 additions and 12 deletions

View File

@@ -47,7 +47,9 @@ from django_scopes import scope, scopes_disabled
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Event, Order, OrderPayment, Organizer, Quota
from pretix.base.models import (
Event, Invoice, Order, OrderPayment, Organizer, Quota,
)
from pretix.base.payment import PaymentException
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException
@@ -113,20 +115,42 @@ def _find_order_for_code(base_qs, code):
pass
def _find_order_for_invoice_id(base_qs, prefix, number):
try:
# Working with __iregex here is an experiment, if this turns out to be too slow in production
# we might need to switch to a different approach.
return base_qs.select_related('order').get(
prefix__istartswith=prefix, # redundant, but hopefully makes it a little faster
full_invoice_no__iregex=prefix + r'[\- ]*0*' + number
).order
except (Invoice.DoesNotExist, Invoice.MultipleObjectsReturned):
pass
@transaction.atomic
def _handle_transaction(trans: BankTransaction, matches: tuple, event: Event = None, organizer: Organizer = None):
orders = []
if event:
for slug, code in matches:
order = _find_order_for_code(event.orders, code)
if order and order.code not in {o.code for o in orders}:
orders.append(order)
if order:
if order.code not in {o.code for o in orders}:
orders.append(order)
else:
order = _find_order_for_invoice_id(Invoice.objects.filter(event=event), slug, code)
if order and order.code not in {o.code for o in orders}:
orders.append(order)
else:
qs = Order.objects.filter(event__organizer=organizer)
for slug, code in matches:
order = _find_order_for_code(qs.filter(event__slug__iexact=slug), code)
if order and order.code not in {o.code for o in orders}:
orders.append(order)
if order:
if order.code not in {o.code for o in orders}:
orders.append(order)
else:
order = _find_order_for_invoice_id(Invoice.objects.filter(event__organizer=organizer), slug, code)
if order and order.code not in {o.code for o in orders}:
orders.append(order)
if not orders:
# No match
@@ -283,24 +307,44 @@ def process_banktransfers(self, job: int, data: list) -> None:
transactions = _get_unknown_transactions(job, data, **job.owner_kwargs)
# Match order codes
code_len_agg = Order.objects.filter(event__organizer=job.organizer).annotate(
clen=Length('code')
).aggregate(min=Min('clen'), max=Max('clen'))
if job.event:
prefixes = [job.event.slug.upper()]
prefixes = {job.event.slug.upper()}
else:
prefixes = [e.slug.upper()
for e in job.organizer.events.all()]
prefixes = {e.slug.upper() for e in job.organizer.events.all()}
# Match invoice numbers
inr_len_agg = Invoice.objects.filter(event__organizer=job.organizer).annotate(
clen=Length('invoice_no')
).aggregate(min=Min('clen'), max=Max('clen'))
if job.event:
prefixes |= {p.rstrip(' -') for p in Invoice.objects.filter(event=job.event).distinct().values_list('prefix', flat=True)}
else:
prefixes |= {p.rstrip(' -') for p in Invoice.objects.filter(event__organizer=job.organizer).distinct().values_list('prefix', flat=True)}
pattern = re.compile(
"(%s)[ \\-_]*([A-Z0-9]{%s,%s})" % (
"|".join(p.replace(".", r"\.").replace("-", r"[\- ]*") for p in prefixes),
code_len_agg['min'] or 0,
code_len_agg['max'] or 5
"|".join(re.escape(p).replace("\\-", r"[\- ]*") for p in prefixes),
min(code_len_agg['min'] or 1, inr_len_agg['min'] or 1),
max(code_len_agg['max'] or 5, inr_len_agg['max'] or 5)
)
)
for trans in transactions:
matches = pattern.findall(trans.reference.replace(" ", "").replace("\n", "").upper())
# Whitespace in references is unreliable since linebreaks and spaces can occur almost anywhere, e.g.
# DEMOCON-123\n45 should be matched to DEMOCON-12345. However, sometimes whitespace is important,
# e.g. when there are two references. "DEMOCON-12345 DEMOCON-45678" would otherwise be parsed as
# "DEMOCON-12345DE" in some conditions. We'll naively take whatever has more matches.
matches_with_whitespace = pattern.findall(trans.reference.replace("\n", " ").upper())
matches_without_whitespace = pattern.findall(trans.reference.replace(" ", "").replace("\n", "").upper())
if len(matches_without_whitespace) > len(matches_with_whitespace):
matches = matches_without_whitespace
else:
matches = matches_with_whitespace
if matches:
if job.event:

View File

@@ -46,6 +46,7 @@ from pretix.base.models import (
Event, Item, Order, OrderFee, OrderPayment, OrderPosition, Organizer,
Quota, Team, User,
)
from pretix.base.services.invoices import generate_invoice
from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction
from pretix.plugins.banktransfer.tasks import process_banktransfers
@@ -57,6 +58,8 @@ def env():
organizer=o, name='Dummy', slug='dummy',
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
)
event.settings.invoice_numbers_prefix = 'INV-'
event.settings.invoice_numbers_counter_length = 3
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
t = Team.objects.create(organizer=event.organizer, can_view_orders=True, can_change_orders=True)
t.members.add(user)
@@ -83,6 +86,10 @@ def env():
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
quota.items.add(item1)
OrderPosition.objects.create(order=o1, item=item1, variation=None, price=23)
i1 = generate_invoice(o1)
assert i1.full_invoice_no == 'INV-001'
i2 = generate_invoice(o2)
assert i2.full_invoice_no == 'INV-002'
return event, user, o1, o2
@@ -232,6 +239,42 @@ def test_autocorrection(env, job):
assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_invoice_id(env, job):
process_banktransfers(job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellung INV-001',
'amount': '23.00',
'date': '2016-01-26',
}])
env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_invoice_id_missing_separator(env, job):
process_banktransfers(job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellung INV001',
'amount': '23.00',
'date': '2016-01-26',
}])
env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_invoice_id_missing_zeros(env, job):
process_banktransfers(job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellung INV1',
'amount': '23.00',
'date': '2016-01-26',
}])
env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_random_spaces(env, job):
process_banktransfers(job, [{