Source code for feriennet.views.invoice

from decimal import Decimal
from datetime import date
from markupsafe import Markup
from onegov.activity import (
    Period, Invoice, InvoiceItem, InvoiceCollection, PeriodCollection)
from onegov.core.security import Personal, Secret
from onegov.core.templates import render_macro
from onegov.feriennet import FeriennetApp, _
from onegov.feriennet.collections import BillingCollection
from onegov.feriennet.forms import DonationForm
from onegov.feriennet.layout import DonationLayout
from onegov.feriennet.layout import InvoiceLayout
from onegov.feriennet.qrbill import generate_qr_bill
from onegov.feriennet.views.shared import users_for_select_element
from onegov.pay import process_payment, INSUFFICIENT_FUNDS
from onegov.user import User
from sqlalchemy.orm import contains_eager
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.sql.expression import case
from stdnum import iban  # type:ignore[import-untyped]
from uuid import UUID


from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from onegov.core.types import RenderData
    from onegov.feriennet.request import FeriennetRequest
    from onegov.pay import Price
    from webob import Response


@FeriennetApp.view(
    model=InvoiceItem,
    permission=Personal,
)
[docs] def redirect_to_invoice_view( self: InvoiceItem, request: 'FeriennetRequest' ) -> 'Response': return request.redirect( request.link( InvoiceCollection( request.session, user_id=self.invoice.user_id, period_id=self.invoice.period_id ) ) )
@FeriennetApp.view( model=InvoiceItem, permission=Secret, name='online-payments' )
[docs] def view_creditcard_payments( self: InvoiceItem, request: 'FeriennetRequest' ) -> 'Response': return request.redirect(request.class_link( BillingCollection, { 'username': self.invoice.user.username, 'period_id': self.invoice.period_id }, name='online-payments' ))
@FeriennetApp.html( model=InvoiceCollection, template='invoices.pt', permission=Personal)
[docs] def view_my_invoices( self: InvoiceCollection, request: 'FeriennetRequest' ) -> 'RenderData': query = PeriodCollection(request.session).query() query = query.filter(Period.finalized == True) periods = {p.id.hex: p for p in query} # By default, we want to see all the invoices, unless a specific one # is selected. This is a bit of a code-smell, because we usually would # want to set that through the model in the path directive. show_all_invoices = 'invoice' not in request.params q = self.query(ignore_period_id=show_all_invoices is True) q = q.filter(Invoice.period_id.in_(periods.keys())) q = q.outerjoin(Period) q = q.outerjoin(InvoiceItem) q = q.options(contains_eager(Invoice.items)) q = q.order_by( Period.execution_start, Invoice.id, case( [ (InvoiceItem.group == 'donation', 2), (InvoiceItem.family != None, 1), ], else_=0 ), InvoiceItem.group, InvoiceItem.text ) invoices = tuple(q) users = users_for_select_element(request) user = request.session.query(User).filter_by(id=self.user_id).one() assert request.current_user is not None if request.current_user.id == self.user_id: title = _('Invoices') else: title = _('Invoices of ${user}', mapping={ 'user': user.title }) # make sure the invoice is set up with the latest configured reference, # which may result in a new reference record being added - this is done # ad-hoc on an invoice to invoice basis since we do not need a new # reference for an invoice that is never looked at for invoice in invoices: if not invoice.paid: self.schema.link(request.session, invoice) meta = request.app.org.meta if self.schema.name == 'feriennet-v1' or meta.get('bank_qr_bill'): account = meta.get('bank_account') account = account and iban.format(account) else: account = meta.get('bank_esr_participant_number') beneficiary = meta.get('bank_beneficiary') payment_provider = request.app.default_payment_provider qr_bill_enabled = meta.get('bank_qr_bill', False) layout = InvoiceLayout(self, request, title) def payment_button(title: str, price: 'Price | None') -> str | None: assert payment_provider is not None assert request.locale is not None price = payment_provider.adjust_price(price) label = ': '.join(( request.translate(_('Pay Online Now')), render_macro(layout.macros['price'], layout.request, { 'layout': layout, 'price': price, 'show_fee': True }) )) return request.app.checkout_button( button_label=label, title=title, price=price, email=user.username, locale=request.locale ) def user_select_link(user: User) -> str: return request.class_link(InvoiceCollection, { 'username': user.username, }) def qr_bill(invoice: Invoice) -> bytes | None: return generate_qr_bill(self.schema.name, request, user, invoice) return { 'title': title, 'layout': layout, 'users': users, 'user': user, 'user_select_link': user_select_link, 'invoices': invoices, 'model': self, 'account': account, 'payment_provider': payment_provider, 'payment_button': payment_button, 'beneficiary': beneficiary, 'invoice_bucket': request.app.invoice_bucket(), 'qr_bill': qr_bill if qr_bill_enabled else None, }
@FeriennetApp.view( model=InvoiceCollection, template='invoices.pt', permission=Personal, request_method='POST')
[docs] def handle_payment( self: InvoiceCollection, request: 'FeriennetRequest' ) -> 'Response': provider = request.app.default_payment_provider assert provider is not None token = request.params.get('payment_token') assert token is None or isinstance(token, str) # FIXME: Can period actually be omitted, i.e. are there # cases where we only get a single Invoice when we # omit the period? period = request.params.get('period') assert period is None or isinstance(period, str) period_id = UUID(period) if period else None invoice = ( self.for_period_id(period_id=period_id).query() .outerjoin(InvoiceItem) .one() ) price = provider.adjust_price(invoice.price) assert price is not None payment = process_payment('cc', price, provider, token) if payment == INSUFFICIENT_FUNDS: request.alert(_('Your card has insufficient funds')) elif payment is None: request.alert(_('Your payment could not be processed')) else: for item in invoice.items: if item.paid: continue item.payments.append(payment) item.source = provider.type item.payment_date = date.today() item.paid = True request.success(_('Your payment has been received. Thank you!')) return request.redirect(request.link(self))
@FeriennetApp.form( model=InvoiceCollection, form=DonationForm, template='donation.pt', permission=Personal, name='donation')
[docs] def handle_donation( self: InvoiceCollection, request: 'FeriennetRequest', form: DonationForm ) -> 'RenderData | Response': assert request.current_user is not None if not self.user_id: return request.redirect(request.link( self.for_user_id(request.current_user.id), name='donation' )) if not self.period_id: assert request.app.active_period is not None return request.redirect(request.link( self.for_period_id(request.app.active_period.id), name='donation' )) if request.current_user.id == self.user_id: title = _('Donation') else: user = request.session.query(User).filter_by(id=self.user_id).one() title = _('Donation of ${user}', mapping={'user': user.title}) period = request.app.periods_by_id[self.period_id.hex] bills = BillingCollection(request, period) if form.submitted(request): try: bills.include_donation( user_id=self.user_id, amount=Decimal(form.amount.data), text=request.translate(_('Donation'))) except NoResultFound: request.alert(_('No invoice found')) else: request.success(_('Thank you for your donation')) return request.redirect(request.link(self)) elif not request.POST: donation = ( request.session.query(InvoiceItem) .filter(InvoiceItem.group == 'donation') .filter( InvoiceItem.invoice_id.in_( request.session.query(Invoice.id) .filter(Invoice.period_id == self.period_id) .filter(Invoice.user_id == self.user_id) ) ).first() ) if donation: amount = f'{donation.amount:.2f}' for key, value in form.amount.choices: # type:ignore[misc] if key == amount: form.amount.data = amount break description = request.app.org.meta.get('donation_description', '').strip() # NOTE: We need treat this as Markup # TODO: It would be cleaner if we had a proxy object # with all the settings as dict_property description = Markup(description) # noqa: RUF035 return { 'title': title, 'layout': DonationLayout(self, request, title), 'form': form, 'button_text': _('Donate'), 'description': description, }
@FeriennetApp.view( model=InvoiceCollection, permission=Personal, name='donation', request_method='DELETE')
[docs] def handle_delete_donation( self: InvoiceCollection, request: 'FeriennetRequest' ) -> None: assert self.user_id and self.period_id request.assert_valid_csrf_token() period = request.app.periods_by_id[self.period_id.hex] bills = BillingCollection(request, period) if bills.exclude_donation(self.user_id): request.success(_('Your donation was removed')) else: request.alert(_('This donation has already been paid'))