def render_invoice_as_html(invoice): assert isinstance(invoice, Invoice) items = invoice.invoice_items() for item in items: item['net_price'] = normalize_price( item['price'] / (1 + invoice.vat.value / 100) ) # TODO this is copied as-is from assopy/views.py, but can be simplified # TODO: also if there are any images included in the invoice make sure to # base64 them. order = invoice.order address = '%s, %s' % (order.address, unicode(order.country)) # TODO: why, instead of passing invoice objects, it explicitly passes # every attribute? ctx = { # TODO: get it from Conference instance 'conference_name': "EuroPython 2018", "conference_location": EP_CITY_FOR_YEAR[invoice.emit_date.year], "bank_info": "", "currency": invoice.local_currency, 'document': ('Fattura N.', 'Invoice N.'), 'title': unicode(invoice), 'code': invoice.code, 'emit_date': invoice.emit_date, 'order': { 'card_name': order.card_name, 'address': address, 'billing_notes': order.billing_notes, 'cf_code': order.cf_code, 'vat_number': order.vat_number, }, 'items': items, 'note': invoice.note, 'price': { 'net': invoice.net_price(), 'vat': invoice.vat_value(), 'total': invoice.price, }, 'vat': invoice.vat, 'is_real_invoice': is_real_invoice_code(invoice.code), "issuer": invoice.issuer, "invoice": invoice, "additional_text": ADDITIONAL_TEXT_FOR_YEAR[invoice.emit_date.year] } return render_to_string('assopy/invoice.html', ctx)
def test_invoices_from_buying_tickets(client): """ This is an example of a full flow, of creating and buying a new ticket. """ # because of 2018 we need to make sure that ECB rates are in place responses.add(responses.GET, DAILY_ECB_URL, body=EXAMPLE_ECB_DAILY_XML) fetch_and_store_latest_ecb_exrates() assert settings.P3_FARES_ENABLED # 1. First create a user with complete profile. # default password is 'password123' per django_factory_boy user = auth_factories.UserFactory(email='*****@*****.**', is_active=True) # both are required to access user profile page. AssopyUserFactory(user=user) AttendeeProfile.objects.create(user=user, slug='foobar') client.login(email='*****@*****.**', password='******') # 2. Let's start with checking if no tickets are available at first cart_url = reverse('p3-cart') response = client.get(cart_url) assert template_used(response, "p3/cart.html") assert 'Sorry, no tickets are available' in response.content # 3. p3/cart.html is using {% fares_available %} assignment tag to display # fares. For more details about fares check conference/fares.py ticket_price = Decimal(100) ticket_amount = 20 social_event_price = Decimal(10) social_event_amount = 5 vat_rate_10, _ = Vat.objects.get_or_create(value=10) vat_rate_20, _ = Vat.objects.get_or_create(value=20) today = date.today() yesterday, tomorrow = today - timedelta(days=1), today + timedelta(days=1) CONFERENCE = settings.CONFERENCE_CONFERENCE create_fare_for_conference( code="TRSP", # Ticket Regular Standard Personal conference=CONFERENCE, price=ticket_price, start_validity=yesterday, end_validity=tomorrow, vat_rate=vat_rate_10) create_fare_for_conference(code=SOCIAL_EVENT_FARE_CODE, conference=CONFERENCE, price=social_event_price, start_validity=yesterday, end_validity=tomorrow, vat_rate=vat_rate_20) # 4. If Fare is created we should have one input on the cart. response = client.get(cart_url) assert template_used(response, "p3/cart.html") _response_content = response.content.decode('utf-8') assert 'Sorry, no tickets are available' not in _response_content assert 'Buy tickets (1 of 2)' in _response_content # There are plenty of tds but only TRSP should have data-fare set assert 'td class="fare" data-fare="TRSP">' in _response_content assert 'td class="fare" data-fare="TDCP">' not in _response_content assert 'td class="fare" data-fare="">' in _response_content # social events assert 'td class="fare" data-fare="VOUPE03">' in _response_content # and one input for TRSP where you can specify how many tickets # TODO: maybe it should have a different type than text? assert '<input type="text" size="2" name="TRSP"' in _response_content # 5. Try buying some tickets # FIXME: looks like the max_tickets is enforced only with javascript assert ticket_amount > conference_settings.MAX_TICKETS response = client.post( cart_url, { 'order_type': 'non-deductible', # == Personal 'TRSP': ticket_amount, 'VOUPE03': social_event_amount, }, follow=True) billing_url = reverse('p3-billing') assert response.status_code == 200 assert response.request['PATH_INFO'] == billing_url assert 'Buy tickets (2 of 2)' in response.content.decode('utf-8') # unless you POST to the billing page the Order is not created assert Order.objects.count() == 0 Country.objects.create(iso='PL', name='Poland') response = client.post(billing_url, { 'card_name': 'Joe Doe', 'payment': 'cc', 'country': 'PL', 'address': 'Random 42', 'cf_code': '31447', 'code_conduct': True, }, follow=True) assert response.status_code == 200 assert response.request['PATH_INFO'] == '/accounts/stripe/checkout/1/' order = Order.objects.get() # FIXME: confirming that max_tickets is only enforced in javascript assert order.orderitem_set.all().count() ==\ ticket_amount + social_event_amount # need to create an email template that's used in the purchasing process Email.objects.create(code='purchase-complete') # no invoices assert Invoice.objects.all().count() == 0 # static date, because of #592 choosing something in 2018 SOME_RANDOM_DATE = date(2018, 1, 1) order.confirm_order(SOME_RANDOM_DATE) assert order.payment_date == SOME_RANDOM_DATE # # multiple items per invoice, one invoice per vat rate. # # 2 invoices but they are both placeholders assert Invoice.objects.all().count() == 2 assert Invoice.objects.filter( html=VAT_NOT_AVAILABLE_PLACEHOLDER).count() == 2 # # and we can then upgrade all invoices to non-placeholders for _invoice in Invoice.objects.all(): upgrade_invoice_placeholder_to_real_invoice(_invoice) assert Invoice.objects.all().count() == 2 assert Invoice.objects.filter( html=VAT_NOT_AVAILABLE_PLACEHOLDER).count() == 0 invoice_vat_10 = Invoice.objects.get(vat__value=10) invoice_vat_20 = Invoice.objects.get(vat__value=20) # only one orderitem_set instance because they are grouped by fare_code # items are ordered desc by price. expected_invoice_items_vat_10 = [ { 'count': ticket_amount, 'price': ticket_price * ticket_amount, 'code': u'TRSP', 'description': u'ep2018 - Regular Standard Personal' }, ] expected_invoice_items_vat_20 = [ { 'count': social_event_amount, 'price': social_event_price * social_event_amount, 'code': SOCIAL_EVENT_FARE_CODE, 'description': u'ep2018 - Social Event' }, ] assert sequence_equals(invoice_vat_10.invoice_items(), expected_invoice_items_vat_10) assert sequence_equals(invoice_vat_20.invoice_items(), expected_invoice_items_vat_20) # check numbers for vat 10% gross_price_vat_10 = ticket_price * ticket_amount net_price_vat_10 = normalize_price(gross_price_vat_10 / Decimal('1.1')) vat_value_vat_10 = gross_price_vat_10 - net_price_vat_10 assert invoice_vat_10.price == gross_price_vat_10 assert invoice_vat_10.net_price() == net_price_vat_10 assert invoice_vat_10.vat_value() == vat_value_vat_10 assert invoice_vat_10.html.startswith('<!DOCTYPE') assert len(invoice_vat_10.html) > 1000 # large html blob # check numbers for vat 20% gross_price_vat_20 = social_event_price * social_event_amount net_price_vat_20 = normalize_price(gross_price_vat_20 / Decimal('1.2')) vat_value_vat_20 = gross_price_vat_20 - net_price_vat_20 assert invoice_vat_20.price == gross_price_vat_20 assert invoice_vat_20.net_price() == net_price_vat_20 assert invoice_vat_20.vat_value() == vat_value_vat_20 assert invoice_vat_20.html.startswith('<!DOCTYPE') assert len(invoice_vat_20.html) > 1000 # large html blob # each OrderItem should have a corresponding Ticket assert Ticket.objects.all().count() == ticket_amount + social_event_amount # Check if user profile has the tickets and invoices available profile_url = reverse('assopy-profile') response = client.get(profile_url) # order code depends on when this test is run, but invoice code should # default to whatever payment_date is (in this case 2018, 1, 1) # TODO: currently this test is under freezegun, but we may want to remove # it later and replace with APIs that allows to control/specify date for # order and invoice. assert 'O/18.0001' in response.content.decode('utf-8') # there is only one order but two invoices assert 'I/18.0001' in response.content.decode('utf-8') assert 'I/18.0002' in response.content.decode('utf-8')
def price_in_local_currency(self): return normalize_price(self.price * self.exchange_rate)
def net_price(self): return normalize_price(self.price / (1 + self.vat.value / 100))
def create_invoices_for_order(order, force_placeholder=False): assert isinstance(order, Order) payment_date = order.payment_date emit_date = payment_date if payment_date else order.created prefix = REAL_INVOICE_PREFIX if payment_date else FAKE_INVOICE_PREFIX # First transaction takes care of "create all invoices or nothing" # Making it more reliable and threadsafe with transaction.atomic(): invoices = [] for vat_item in order.vat_list(): # Second, nested, transaction is here because otherwise getting new # invoice_code wouldn't work with database fetch, we would need to # increment it manually. (hence another transaction per item) with transaction.atomic(): code = next_invoice_code_for_year( prefix=prefix, year=emit_date.year ) gross_price = vat_item['price'] vat_rate = normalize_price(1 + vat_item['vat'].value / 100) net_price = normalize_price(vat_item['price'] / vat_rate) vat_price = vat_item['price'] - net_price currency = LOCAL_CURRENCY_BY_YEAR[emit_date.year] if currency != 'EUR': conversion = convert_from_EUR_using_latest_exrates( vat_price, currency ) else: conversion = { 'currency': 'EUR', 'converted': vat_price, 'exrate': Decimal('1.0'), 'using_exrate_date': emit_date, } invoice, _ = Invoice.objects.update_or_create( order=order, code=code, defaults={ 'issuer': ISSUER_BY_YEAR[emit_date.year], 'vat': vat_item['vat'], 'price': gross_price, 'payment_date': payment_date, 'emit_date': emit_date, 'local_currency': currency, 'vat_in_local_currency': conversion['converted'], 'exchange_rate': conversion['exrate'], 'exchange_rate_date': conversion['using_exrate_date'], } ) if force_placeholder: invoice.html = VAT_NOT_AVAILABLE_PLACEHOLDER else: invoice.html = render_invoice_as_html(invoice) invoice.save() assert invoice.net_price() == net_price assert invoice.vat_value() == vat_price invoices.append(invoice) return invoices