forked from CGM_Public/pretix_original
Bank transfer: Match orders based on invoice number (#2867)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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, [{
|
||||
|
||||
Reference in New Issue
Block a user