Пример #1
0
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)
Пример #2
0
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')
Пример #3
0
 def price_in_local_currency(self):
     return normalize_price(self.price * self.exchange_rate)
Пример #4
0
 def net_price(self):
     return normalize_price(self.price / (1 + self.vat.value / 100))
Пример #5
0
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