Exemplo n.º 1
0
def send_certificate_email(request):
    """
    Send email to a member with a link
    so the member can get her membership certificate.
    """
    _special_condition = False  # for redirects to referrer

    mid = request.matchdict["id"]
    member = C3sMember.get_by_id(mid)
    if isinstance(member, NoneType) or not member.is_member():
        return Response("that id does not exist or is not an accepted member. go back", status="404 Not Found")
    # create a token for the certificate
    member.certificate_token = make_random_token()

    email_subject, email_body = make_membership_certificate_email(request, member)

    the_message = Message(subject=email_subject, sender="*****@*****.**", recipients=[member.email], body=email_body)
    send_message(request, the_message)

    member.certificate_email = True
    member.certificate_email_date = datetime.now()

    try:  # pragma: no cover
        if "detail" in request.referrer:
            _special_condition = True
    except TypeError:  # pragma: no cover
        pass

    if _special_condition:  # pragma: no cover
        return HTTPFound(location=request.referrer + "#certificate")
    else:
        return get_memberhip_listing_redirect(request, member.id)
Exemplo n.º 2
0
    def mail_signature_confirmation(self, member_id, request):
        """
        Sends an email to the member in order to confirm that the signed
        contract was received by the C3S.

        Args:
            member_id (int): The ID of the member to which the confirmation
                email is sent.
        """
        # TODO:
        # - Email functionality should be injected to be testable!
        # - Email functionality is an external service which belongs to
        #   cross-cutting concerns.
        # - Emailing service should be independent of the presentation layer,
        #   i.e. independent from pyramid which makes it hard to use
        #   pyramid_mailer.
        # - Resolve request dependency.
        # - Remove dependency to pyramid_mail and move to separate service.
        member = self.member_repository.get_member_by_id(member_id)
        # pylint: disable=too-many-function-args
        email_subject, email_body = make_signature_confirmation_email(member)
        message = Message(
            subject=email_subject,
            sender='*****@*****.**',
            recipients=[member.email],
            body=email_body
        )
        # pylint: disable=too-many-function-args
        send_message(request, message)
        member.signature_confirmed = True
        member.signature_confirmed_date = self.datetime.now()
Exemplo n.º 3
0
def mail_payment_reminder(request):
    """
    Send a mail to a membership applicant
    reminding her about lack of **payment**.
    Headquarters is still waiting for the **bank transfer**.

    This view can only be used by staff.

    To be approved for membership applicants have to

    * **Transfer money** for the shares to acquire (at least one share).
    * Send the signed form back to headquarters.
    """
    member = request.registry.member_information.get_member_by_id(
        request.matchdict['memberid'])

    email_subject, email_body = make_payment_reminder_email(member)
    message = Message(subject=email_subject,
                      sender='*****@*****.**',
                      recipients=[member.email],
                      body=email_body)
    send_message(request, message)

    try:  # if value is int
        member.sent_payment_reminder += 1
    except TypeError:  # pragma: no cover
        # if value was None (after migration of DB schema)
        member.sent_payment_reminder = 1
    member.sent_payment_reminder_date = datetime.now()
    if 'detail' in request.referrer:
        return HTTPFound(
            request.route_url('detail',
                              memberid=request.matchdict['memberid']))
    else:
        return get_dashboard_redirect(request, member.id)
def mail_payment_confirmation(request):
    """
    Send a mail to a membership applicant
    informing her about reception of payment.
    """
    member = request.registry.member_information.get_member_by_id(
        request.matchdict['member_id'])

    email_subject, email_body = make_payment_confirmation_email(member)
    message = Message(
        subject=email_subject,
        sender='*****@*****.**',
        recipients=[member.email],
        body=email_body,
    )
    send_message(request, message)

    member.payment_confirmed = True
    member.payment_confirmed_date = datetime.now()
    if 'detail' in request.referrer:
        return HTTPFound(
            request.route_url('detail',
                              memberid=request.matchdict['memberid']))
    else:
        return get_dashboard_redirect(request, member.id)
Exemplo n.º 5
0
def send_certificate_email(request):
    '''
    Send email to a member with a link
    so the member can get her membership certificate.
    '''
    member = request.validated_matchdict['member']

    member.certificate_token = make_random_token()
    email_subject, email_body = make_membership_certificate_email(
        request,
        member)
    the_message = Message(
        subject=email_subject,
        sender=request.registry.settings['c3smembership.notification_sender'],
        recipients=[member.email],
        body=email_body
    )
    send_message(request, the_message)

    member.certificate_email = True
    member.certificate_email_date = datetime.now()

    if hasattr(request, 'referer') and request.referer is not None and \
            'detail' in request.referer:
        return HTTPFound(
            request.route_url(
                'detail',
                member_id=member.id,
                _anchor='certificate'
            )
        )
    else:
        return get_memberhip_listing_redirect(request, member.id)
Exemplo n.º 6
0
def mail_signature_reminder(request):
    """
    Send a mail to a membership applicant
    reminding her about lack of *signature*.
    Headquarters is still waiting for the *signed form*.

    This view can only be used by staff.

    To be approved for membership applicants have to

    * Transfer money for the shares to acquire (at least one share).
    * **Send the signed form** back to headquarters.
    """
    member_id = request.matchdict['memberid']
    member = C3sMember.get_by_id(member_id)
    if isinstance(member, NoneType):
        request.session.flash(
            'that member was not found! (id: {})'.format(member_id),
            'messages'
        )
        return get_dashboard_redirect(request, member.id)

    email_subject, email_body = make_signature_reminder_email(member)
    message = Message(
        subject=email_subject,
        sender='*****@*****.**',
        recipients=[member.email],
        body=email_body
    )
    send_message(request, message)

    try:
        member.sent_signature_reminder += 1
    except TypeError:
        # if value was None (after migration of DB schema)
        member.sent_signature_reminder = 1
    member.sent_signature_reminder_date = datetime.now()
    if 'detail' in request.referrer:
        return HTTPFound(request.route_url(
            'detail',
            memberid=request.matchdict['memberid']))
    else:
        return get_dashboard_redirect(request, member.id)
Exemplo n.º 7
0
def invite_member_bcgv(request):
    """
    Send email to member with link to ticketing.

    === ====================================
    URL http://app:port/invite_member/{m_id}
    === ====================================
    """
    member_id = request.matchdict['m_id']
    member = C3sMember.get_by_id(member_id)
    if isinstance(member, NoneType):
        request.session.flash('id not found. no mail sent.', 'messages')
        return get_memberhip_listing_redirect(request)

    if not member.is_member():
        request.session.flash('Invitations can only be sent to members.',
                              'messages')
        return get_memberhip_listing_redirect(request, member_id)

    # prepare a random token iff none is set
    if member.email_invite_token_bcgv17 is None:
        member.email_invite_token_bcgv17 = make_random_token()
    url = URL_PATTERN.format(
        ticketing_url=request.registry.settings['ticketing.url'],
        token=member.email_invite_token_bcgv17,
        email=member.email)

    LOG.info("mailing event invitation to to member id %s", member.id)

    email_subject, email_body = make_bcga17_invitation_email(member, url)
    message = Message(subject=email_subject,
                      sender='*****@*****.**',
                      recipients=[member.email],
                      body=email_body,
                      extra_headers={
                          'Reply-To': '*****@*****.**',
                      })
    send_message(request, message)

    # member._token = _looong_token
    member.email_invite_flag_bcgv17 = True
    member.email_invite_date_bcgv17 = datetime.now()
    return get_memberhip_listing_redirect(request, member.id)
Exemplo n.º 8
0
 def mail_signature_confirmation(self, member_id, request):
     # TODO:
     # - Email functionality should be injected to be testable!
     # - Email functionality is an external service which belongs to
     #   cross-cutting concerns.
     # - Emailing service should be independent of the presentation layer,
     #   i.e. independent from pyramid which makes it hard to use
     #   pyramid_mailer.
     member = self.c3s_member.get_by_id(member_id)
     email_subject, email_body = make_signature_confirmation_email(member)
     # TODO: Remove dependency to pyramid_mail.
     message = Message(
         subject=email_subject,
         sender='*****@*****.**',
         recipients=[member.email],
         body=email_body
     )
     # TODO: Resolve request dependency.
     send_message(request, message)
     member.signature_confirmed = True
     member.signature_confirmed_date = datetime.now()
Exemplo n.º 9
0
def mail_signature_reminder(request):
    """
    Send a mail to a membership applicant
    reminding her about lack of *signature*.
    Headquarters is still waiting for the *signed form*.

    This view can only be used by staff.

    To be approved for membership applicants have to

    * Transfer money for the shares to acquire (at least one share).
    * **Send the signed form** back to headquarters.
    """
    member_id = request.matchdict['memberid']
    member = C3sMember.get_by_id(member_id)
    if isinstance(member, NoneType):
        request.session.flash(
            'that member was not found! (id: {})'.format(member_id),
            'messages')
        return get_dashboard_redirect(request, member.id)

    email_subject, email_body = make_signature_reminder_email(member)
    message = Message(subject=email_subject,
                      sender='*****@*****.**',
                      recipients=[member.email],
                      body=email_body)
    send_message(request, message)

    try:
        member.sent_signature_reminder += 1
    except TypeError:
        # if value was None (after migration of DB schema)
        member.sent_signature_reminder = 1
    member.sent_signature_reminder_date = datetime.now()
    if 'detail' in request.referrer:
        return HTTPFound(
            request.route_url('detail',
                              memberid=request.matchdict['memberid']))
    else:
        return get_dashboard_redirect(request, member.id)
def send_certificate_email(request):
    '''
    Send email to a member with a link
    so the member can get her membership certificate.
    '''
    _special_condition = False  # for redirects to referrer

    mid = request.matchdict['id']
    member = C3sMember.get_by_id(mid)
    if isinstance(member, NoneType) or not member.is_member():
        return Response(
            'that id does not exist or is not an accepted member. go back',
            status='404 Not Found',
        )
    # create a token for the certificate
    member.certificate_token = make_random_token()

    email_subject, email_body = make_membership_certificate_email(
        request, member)

    the_message = Message(subject=email_subject,
                          sender='*****@*****.**',
                          recipients=[member.email],
                          body=email_body)
    send_message(request, the_message)

    member.certificate_email = True
    member.certificate_email_date = datetime.now()

    try:  # pragma: no cover
        if 'detail' in request.referrer:
            _special_condition = True
    except TypeError:  # pragma: no cover
        pass

    if _special_condition:  # pragma: no cover
        return HTTPFound(location=request.referrer + '#certificate')
    else:
        return get_memberhip_listing_redirect(request, member.id)
Exemplo n.º 11
0
def mail_payment_reminder(request):
    """
    Send a mail to a membership applicant
    reminding her about lack of **payment**.
    Headquarters is still waiting for the **bank transfer**.

    This view can only be used by staff.

    To be approved for membership applicants have to

    * **Transfer money** for the shares to acquire (at least one share).
    * Send the signed form back to headquarters.
    """
    member = request.validated_matchdict['member']

    email_subject, email_body = make_payment_reminder_email(member)
    message = Message(
        subject=email_subject,
        sender=request.registry.settings['c3smembership.notification_sender'],
        recipients=[member.email],
        body=email_body
    )
    send_message(request, message)

    if member.sent_payment_reminder is None or \
            member.sent_payment_reminder == 0:
        member.sent_payment_reminder = 1
    else:
        member.sent_payment_reminder += 1
    member.sent_payment_reminder_date = datetime.now()

    if request.referer is not None and 'detail' in request.referer:
        return HTTPFound(request.route_url(
            'detail',
            member_id=member.id))
    else:
        return get_dashboard_redirect(request, member.id)
Exemplo n.º 12
0
def mail_payment_confirmation(request):
    """
    Send a mail to a membership applicant
    informing her about reception of payment.
    """
    member = request.validated_matchdict['member']

    email_subject, email_body = make_payment_confirmation_email(member)
    message = Message(
        subject=email_subject,
        sender=request.registry.settings['c3smembership.notification_sender'],
        recipients=[member.email],
        body=email_body,
    )
    send_message(request, message)

    member.payment_confirmed = True
    member.payment_confirmed_date = datetime.now()
    if request.referer is not None and 'detail' in request.referer:
        return HTTPFound(request.route_url(
            'detail',
            member_id=member.id))
    else:
        return get_dashboard_redirect(request, member.id)
Exemplo n.º 13
0
def mail_payment_reminder(request):
    """
    Send a mail to a membership applicant
    reminding her about lack of **payment**.
    Headquarters is still waiting for the **bank transfer**.

    This view can only be used by staff.

    To be approved for membership applicants have to

    * **Transfer money** for the shares to acquire (at least one share).
    * Send the signed form back to headquarters.
    """
    member = C3sMember.get_by_id(request.matchdict['memberid'])

    email_subject, email_body = make_payment_reminder_email(member)
    message = Message(
        subject=email_subject,
        sender='*****@*****.**',
        recipients=[member.email],
        body=email_body
    )
    send_message(request, message)

    try:  # if value is int
        member.sent_payment_reminder += 1
    except TypeError:  # pragma: no cover
        # if value was None (after migration of DB schema)
        member.sent_payment_reminder = 1
    member.sent_payment_reminder_date = datetime.now()
    if 'detail' in request.referrer:
        return HTTPFound(request.route_url(
            'detail',
            memberid=request.matchdict['memberid']))
    else:
        return get_dashboard_redirect(request, member.id)
Exemplo n.º 14
0
def mail_payment_confirmation(request):
    """
    Send a mail to a membership applicant
    informing her about reception of payment.
    """
    member = C3sMember.get_by_id(request.matchdict['memberid'])

    email_subject, email_body = make_payment_confirmation_email(member)
    message = Message(
        subject=email_subject,
        sender='*****@*****.**',
        recipients=[member.email],
        body=email_body,
    )
    send_message(request, message)

    member.payment_confirmed = True
    member.payment_confirmed_date = datetime.now()
    if 'detail' in request.referrer:
        return HTTPFound(request.route_url(
            'detail',
            memberid=request.matchdict['memberid']))
    else:
        return get_dashboard_redirect(request, member.id)
Exemplo n.º 15
0
def dues18_reduction(request):
    """
    reduce a members dues upon valid request to do so.

    * change payable amount for member
    * cancel old invoice by issuing a cancellation
    * issue a new invoice with the new amount (if new amount != 0)
    """
    member_id = request.matchdict.get('member_id')
    member = C3sMember.get_by_id(member_id)  # is in database
    if (member is None or
            not member.membership_accepted or
            not member.dues18_invoice):
        request.session.flash(
            u"Member not found or not a member or no invoice to reduce",
            'dues18_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', member_id=member.id) + '#dues18')

    # sanity check: the given amount is a positive decimal
    try:
        reduced_amount = D(request.POST['amount'])
        assert not reduced_amount.is_signed()
        if DEBUG:
            print("DEBUG: reduction to {}".format(reduced_amount))
    except (KeyError, AssertionError):  # pragma: no cover
        request.session.flash(
            (u"Invalid amount to reduce to: '{}' "
             u"Use the dot ('.') as decimal mark, e.g. '23.42'".format(
                 request.POST['amount'])),
            'dues18_message_to_staff'  # message queue for user
        )
        return HTTPFound(
            request.route_url('detail', member_id=member.id) + '#dues18')

    if DEBUG:
        print("DEBUG: member.dues18_amount: {}".format(
            member.dues18_amount))
        print("DEBUG: type(member.dues18_amount): {}".format(
            type(member.dues18_amount)))
        print("DEBUG: member.dues18_reduced: {}".format(
            member.dues18_reduced))
        print("DEBUG: member.dues18_amount_reduced: {}".format(
            member.dues18_amount_reduced))
        print("DEBUG: type(member.dues18_amount_reduced): {}".format(
            type(member.dues18_amount_reduced)))

    # The hidden input 'confirmed' must have the value 'yes' which is set by
    # the confirmation dialog.
    reduction_confirmed = request.POST['confirmed']
    if reduction_confirmed != 'yes':
        request.session.flash(
            u'Die Reduktion wurde nicht bestätigt.',
            'dues18_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', member_id=member.id) + '#dues18')

    # check the reduction amount: same as default calculated amount?
    if (not member.dues18_reduced  and
            member.dues18_amount == reduced_amount):
        request.session.flash(
            u"Dieser Beitrag ist der default-Beitrag!",
            'dues18_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', member_id=member.id) + '#dues18')

    if (member.dues18_reduced and
            reduced_amount == member.dues18_amount_reduced):
        request.session.flash(
            u"Auf diesen Beitrag wurde schon reduziert!",
            'dues18_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', member_id=member.id) + '#dues18')

    if (member.dues18_reduced and
            reduced_amount > member.dues18_amount_reduced or
            reduced_amount > member.dues18_amount):
        request.session.flash(
            u'Beitrag darf nicht über den berechneten oder bereits'
            u'reduzierten Wert gesetzt werden.',
            'dues18_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', member_id=member.id) + '#dues18')

    # prepare: get highest invoice no from db
    max_invoice_no = DuesInvoiceRepository.get_max_invoice_number(2018)

    # things to be done:
    # * change dues amount for that member
    # * cancel old invoice by issuing a reversal invoice
    # * issue a new invoice with the new amount

    member.set_dues18_reduced_amount(reduced_amount)
    request.session.flash('reduction to {}'.format(reduced_amount),
                          'dues18_message_to_staff')

    old_invoice = DuesInvoiceRepository.get_by_number(
        member.dues18_invoice_no, 2018)
    old_invoice.is_cancelled = True

    reversal_invoice_amount = -D(old_invoice.invoice_amount)

    # prepare reversal invoice number
    new_invoice_no = max_invoice_no + 1
    # create reversal invoice
    reversal_invoice = Dues18Invoice(
        invoice_no=new_invoice_no,
        invoice_no_string=(
            u'C3S-dues2018-' + str(new_invoice_no).zfill(4)) + '-S',
        invoice_date=datetime.today(),
        invoice_amount=reversal_invoice_amount.to_eng_string(),
        member_id=member.id,
        membership_no=member.membership_number,
        email=member.email,
        token=member.dues18_token,
    )
    reversal_invoice.preceding_invoice_no = old_invoice.invoice_no
    reversal_invoice.is_reversal = True
    DBSession.add(reversal_invoice)
    DBSession.flush()
    old_invoice.succeeding_invoice_no = new_invoice_no

    # check if this is an exemption (reduction to zero)
    is_exemption = False  # sane default
    # check if reduction to zero
    if reduced_amount.is_zero():
        is_exemption = True
        if DEBUG:
            print("this is an exemption: reduction to zero")
    else:
        if DEBUG:
            print("this is a reduction to {}".format(reduced_amount))

    if not is_exemption:
        # create new invoice
        new_invoice = Dues18Invoice(
            invoice_no=new_invoice_no + 1,
            invoice_no_string=(
                u'C3S-dues2018-' + str(new_invoice_no + 1).zfill(4)),
            invoice_date=datetime.today(),
            invoice_amount=u'' + str(reduced_amount),
            member_id=member.id,
            membership_no=member.membership_number,
            email=member.email,
            token=member.dues18_token,
        )
        new_invoice.is_altered = True
        new_invoice.preceding_invoice_no = reversal_invoice.invoice_no
        reversal_invoice.succeeding_invoice_no = new_invoice_no + 1
        DBSession.add(new_invoice)

        # in the members record, store the current invoice no
        member.dues18_invoice_no = new_invoice_no + 1

        DBSession.flush()  # persist newer invoices

    reversal_url = (
        request.route_url(
            'make_dues18_reversal_invoice_pdf',
            email=member.email,
            code=member.dues18_token,
            no=str(reversal_invoice.invoice_no).zfill(4)
        )
    )
    if is_exemption:
        email_subject, email_body = make_dues_exemption_email(
            member,
            reversal_url)
    else:
        invoice_url = (
            request.route_url(
                'make_dues18_invoice_no_pdf',
                email=member.email,
                code=member.dues18_token,
                i=str(new_invoice_no + 1).zfill(4)
            )
        )
        email_subject, email_body = make_dues18_reduction_email(
            member,
            new_invoice,
            invoice_url,
            reversal_url)

    message = Message(
        subject=email_subject,
        sender=request.registry.settings[
            'c3smembership.notification_sender'],
        recipients=[member.email],
        body=email_body,
    )
    if is_exemption:
        request.session.flash('exemption email was sent to user!',
                              'dues18_message_to_staff')
    else:
        request.session.flash('update email was sent to user!',
                              'dues18_message_to_staff')
    send_message(request, message)
    return HTTPFound(
        request.route_url(
            'detail',
            member_id=member_id) +
        '#dues18')
Exemplo n.º 16
0
def send_dues18_invoice_email(request, m_id=None):
    """
    Send email to a member to prompt her to pay the membership dues.
    - For normal members, also send link to invoice.
    - For investing members that are legal entities,
      ask for additional support depending on yearly turnover.

    This view function works both if called via URL, e.g. /dues_invoice/123
    and if called as a function with a member id as parameter.
    The latter is useful for batch processing.

    When this function is used for the first time for one member,
    some database fields are filled:
    - Invoice number
    - Invoice amount (calculated from date of membership approval by the board)
    - Invoice token
    Also, the database table of invoices (and cancellations) is appended.

    If this function gets called the second time for a member,
    no new invoice is produced, but the same mail sent again.
    """
    # either we are given a member id via url or function parameter
    try:  # view was called via http/s
        member_id = request.matchdict['member_id']
        batch = False
    except KeyError:  # ...or was called as function with parameter (see batch)
        member_id = m_id
        batch = True

    try:  # get member from DB
        member = C3sMember.get_by_id(member_id)
        assert(member is not None)
    except AssertionError:
        if not batch:
            request.session.flash(
                "member with id {} not found in DB!".format(member_id),
                'warning')
            return HTTPFound(request.route_url('dues'))

    # sanity check:is this a member?
    try:
        assert(member.membership_accepted)  # must be accepted member!
    except AssertionError:
        request.session.flash(
            "member {} not accepted by the board!".format(member_id),
            'warning')
        return HTTPFound(request.route_url('dues'))

    if 'normal' not in member.membership_type and \
            'investing' not in member.membership_type:
        request.session.flash(
            'The membership type of member {0} is not specified! The '
            'membership type must either be "normal" or "investing" in order '
            'to be able to send an invoice email.'.format(member.id),
            'warning')
        return get_memberhip_listing_redirect(request)
    if member.membership_date >= date(2019, 1, 1) or (
                member.membership_loss_date is not None
                and member.membership_loss_date < date(2018, 1, 1)
            ):
        request.session.flash(
            'Member {0} was not a member in 2018. Therefore, you cannot send '
            'an invoice for 2018.'.format(member.id),
            'warning')
        return get_memberhip_listing_redirect(request)

    # check if invoice no already exists.
    #     if yes: just send that email again!
    #     also: offer staffers to cancel this invoice

    if member.dues18_invoice is True:
        invoice = DuesInvoiceRepository.get_by_number(
            member.dues18_invoice_no, 2018)
        member.dues18_invoice_date = datetime.now()

    else:  # if no invoice already exists:
        # make dues token and ...
        randomstring = make_random_string()
        # check if dues token is already used
        while DuesInvoiceRepository.token_exists(randomstring, 2018):
            # create a new one, if the new one already exists in the database
            randomstring = make_random_string()  # pragma: no cover

        # prepare invoice number
        try:
            # either we already have an invoice number for that client...
            invoice_no = member.dues18_invoice_no
            assert invoice_no is not None
        except AssertionError:
            # ... or we create a new one and save it
            # get max invoice no from db
            max_invoice_no = DuesInvoiceRepository.get_max_invoice_number(2018)
            # use the next free number, save it to db
            new_invoice_no = int(max_invoice_no) + 1
            DBSession.flush()  # save dataset to DB

        # calculate dues amount (maybe partial, depending on quarter)
        dues_start, dues_amount = calculate_partial_dues18(member)

        # now we have enough info to update the member info
        # and persist invoice info for bookkeeping
        # store some info in DB/member table
        member.dues18_invoice = True
        member.dues18_invoice_no = new_invoice_no  # irrelevant for investing
        member.dues18_invoice_date = datetime.now()
        member.dues18_token = randomstring
        member.dues18_start = dues_start

        if 'normal' in member.membership_type:  # only for normal members
            member.set_dues18_amount(dues_amount)
            # store some more info about invoice in invoice table
            invoice = Dues18Invoice(
                invoice_no=member.dues18_invoice_no,
                invoice_no_string=(
                    u'C3S-dues2018-' + str(member.dues18_invoice_no).zfill(4)),
                invoice_date=member.dues18_invoice_date,
                invoice_amount=u'' + str(member.dues18_amount),
                member_id=member.id,
                membership_no=member.membership_number,
                email=member.email,
                token=member.dues18_token,
            )
            DBSession.add(invoice)
        DBSession.flush()

    # now: prepare that email
    # only normal (not investing) members *have to* pay the dues.
    # only the normal members get an invoice link and PDF produced for them.
    # only investing legalentities are asked for more support.
    if 'investing' not in member.membership_type:
        start_quarter = string_start_quarter_dues18(member)
        invoice_url = (
            request.route_url(
                'make_dues18_invoice_no_pdf',
                email=member.email,
                code=member.dues18_token,
                i=str(member.dues18_invoice_no).zfill(4)
            )
        )
        email_subject, email_body = make_dues18_invoice_email(
            member,
            invoice,
            invoice_url,
            start_quarter)
        message = Message(
            subject=email_subject,
            sender=request.registry.settings[
                'c3smembership.notification_sender'],
            recipients=[member.email],
            body=email_body,
        )
    elif 'investing' in member.membership_type:
        if member.is_legalentity:
            email_subject, email_body = \
                make_dues_invoice_legalentity_email(member)
        else:
            email_subject, email_body = \
                make_dues_invoice_investing_email(member)
        message = Message(
            subject=email_subject,
            sender=request.registry.settings[
                'c3smembership.notification_sender'],
            recipients=[member.email],
            body=email_body,
        )

    # print to console or send mail
    if 'true' in request.registry.settings['testing.mail_to_console']:
        print(message.body.encode('utf-8'))  # pragma: no cover
    else:
        send_message(request, message)

    # now choose where to redirect
    if 'detail' in request.referrer:
        return HTTPFound(
            request.route_url(
                'detail',
                member_id=member.id) +
            '#dues18')
    if 'dues' in request.referrer:
        return HTTPFound(request.route_url('dues'))
    else:
        return get_memberhip_listing_redirect(request, member.id)
def dues17_reduction(request):
    """
    reduce a members dues upon valid request to do so.

    * change payable amount for member
    * cancel old invoice by issuing a cancellation
    * issue a new invoice with the new amount (if new amount != 0)

    this will only work for *normal* members.
    """
    # member: sanity checks
    try:
        member_id = request.matchdict['member_id']
        member = C3sMember.get_by_id(member_id)  # is in database
        assert member.membership_accepted  # is a member
        assert 'investing' not in member.membership_type  # is normal member
    except (KeyError, AssertionError):  # pragma: no cover
        request.session.flash(
            u"No member OR not accepted OR not normal member",
            'dues17_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', memberid=member.id) + '#dues17')

    # sanity check: the given amount is a positive decimal
    try:
        reduced_amount = D(request.POST['amount'])
        assert not reduced_amount.is_signed()
        if DEBUG:
            print("DEBUG: reduction to {}".format(reduced_amount))
    except (KeyError, AssertionError):  # pragma: no cover
        request.session.flash(
            (u"Invalid amount to reduce to: '{}' "
             u"Use the dot ('.') as decimal mark, e.g. '23.42'".format(
                 request.POST['amount'])),
            'dues17_message_to_staff'  # message queue for user
        )
        return HTTPFound(
            request.route_url('detail', memberid=member.id) + '#dues17')

    if DEBUG:
        print("DEBUG: member.dues17_amount: {}".format(member.dues17_amount))
        print("DEBUG: type(member.dues17_amount): {}".format(
            type(member.dues17_amount)))
        print("DEBUG: member.dues17_reduced: {}".format(member.dues17_reduced))
        print("DEBUG: member.dues17_amount_reduced: {}".format(
            member.dues17_amount_reduced))
        print("DEBUG: type(member.dues17_amount_reduced): {}".format(
            type(member.dues17_amount_reduced)))

    # The hidden input 'confirmed' must have the value 'yes' which is set by
    # the confirmation dialog.
    reduction_confirmed = request.POST['confirmed']
    if reduction_confirmed != 'yes':
        request.session.flash(
            u'Die Reduktion wurde nicht bestätigt.',
            'dues17_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', memberid=member.id) + '#dues17')

    # check the reduction amount: same as default calculated amount?
    if ((member.dues17_reduced is False)
            and (member.dues17_amount == reduced_amount)):
        request.session.flash(
            u"Dieser Beitrag ist der default-Beitrag!",
            'dues17_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', memberid=member.id) + '#dues17')

    if reduced_amount == member.dues17_amount_reduced:
        request.session.flash(
            u"Auf diesen Beitrag wurde schon reduziert!",
            'dues17_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', memberid=member.id) + '#dues17')

    if member.dues17_reduced \
            and reduced_amount > member.dues17_amount_reduced \
            or reduced_amount > member.dues17_amount:
        request.session.flash(
            u'Beitrag darf nicht über den berechneten oder bereits'
            u'reduzierten Wert gesetzt werden.',
            'dues17_message_to_staff'  # message queue for staff
        )
        return HTTPFound(
            request.route_url('detail', memberid=member.id) + '#dues17')

    # prepare: get highest invoice no from db
    max_invoice_no = Dues17Invoice.get_max_invoice_no()

    # things to be done:
    # * change dues amount for that member
    # * cancel old invoice by issuing a reversal invoice
    # * issue a new invoice with the new amount

    member.set_dues17_reduced_amount(reduced_amount)
    request.session.flash('reduction to {}'.format(reduced_amount),
                          'dues17_message_to_staff')

    old_invoice = Dues17Invoice.get_by_invoice_no(member.dues17_invoice_no)
    old_invoice.is_cancelled = True

    reversal_invoice_amount = -D(old_invoice.invoice_amount)

    # prepare reversal invoice number
    new_invoice_no = max_invoice_no + 1
    # create reversal invoice
    reversal_invoice = Dues17Invoice(
        invoice_no=new_invoice_no,
        invoice_no_string=(u'C3S-dues2017-' + str(new_invoice_no).zfill(4)) +
        '-S',
        invoice_date=datetime.today(),
        invoice_amount=reversal_invoice_amount.to_eng_string(),
        member_id=member.id,
        membership_no=member.membership_number,
        email=member.email,
        token=member.dues17_token,
    )
    reversal_invoice.preceding_invoice_no = old_invoice.invoice_no
    reversal_invoice.is_reversal = True
    DBSession.add(reversal_invoice)
    DBSession.flush()
    old_invoice.succeeding_invoice_no = new_invoice_no

    # check if this is an exemption (reduction to zero)
    is_exemption = False  # sane default
    # check if reduction to zero
    if reduced_amount.is_zero():
        is_exemption = True
        if DEBUG:
            print("this is an exemption: reduction to zero")
    else:
        if DEBUG:
            print("this is a reduction to {}".format(reduced_amount))

    if not is_exemption:
        # create new invoice
        new_invoice = Dues17Invoice(
            invoice_no=new_invoice_no + 1,
            invoice_no_string=(u'C3S-dues2017-' +
                               str(new_invoice_no + 1).zfill(4)),
            invoice_date=datetime.today(),
            invoice_amount=u'' + str(reduced_amount),
            member_id=member.id,
            membership_no=member.membership_number,
            email=member.email,
            token=member.dues17_token,
        )
        new_invoice.is_altered = True
        new_invoice.preceding_invoice_no = reversal_invoice.invoice_no
        reversal_invoice.succeeding_invoice_no = new_invoice_no + 1
        DBSession.add(new_invoice)

        # in the members record, store the current invoice no
        member.dues17_invoice_no = new_invoice_no + 1

        DBSession.flush()  # persist newer invoices

    reversal_url = (request.route_url(
        'make_dues17_reversal_invoice_pdf',
        email=member.email,
        code=member.dues17_token,
        no=str(reversal_invoice.invoice_no).zfill(4)))
    if is_exemption:
        email_subject, email_body = make_dues_exemption_email(
            member, reversal_url)
    else:
        invoice_url = (request.route_url('make_dues17_invoice_no_pdf',
                                         email=member.email,
                                         code=member.dues17_token,
                                         i=str(new_invoice_no + 1).zfill(4)))
        email_subject, email_body = make_dues17_reduction_email(
            member, new_invoice, invoice_url, reversal_url)

    message = Message(subject=email_subject,
                      sender='*****@*****.**',
                      recipients=[member.email],
                      body=email_body,
                      extra_headers={
                          'Reply-To': '*****@*****.**',
                      })
    if is_exemption:
        request.session.flash('exemption email was sent to user!',
                              'dues17_message_to_staff')
    else:
        request.session.flash('update email was sent to user!',
                              'dues17_message_to_staff')
    send_message(request, message)
    return HTTPFound(
        request.route_url('detail', memberid=member_id) + '#dues17')
def send_dues17_invoice_email(request, m_id=None):
    """
    Send email to a member to prompt her to pay the membership dues.
    - For normal members, also send link to invoice.
    - For investing members that are legal entities,
      ask for additional support depending on yearly turnover.

    This view function works both if called via URL, e.g. /dues_invoice/123
    and if called as a function with a member id as parameter.
    The latter is useful for batch processing.

    When this function is used for the first time for one member,
    some database fields are filled:
    - Invoice number
    - Invoice amount (calculated from date of membership approval by the board)
    - Invoice token
    Also, the database table of invoices (and cancellations) is appended.

    If this function gets called the second time for a member,
    no new invoice is produced, but the same mail sent again.
    """
    # either we are given a member id via url or function parameter
    try:  # view was called via http/s
        member_id = request.matchdict['member_id']
        batch = False
    except KeyError:  # ...or was called as function with parameter (see batch)
        member_id = m_id
        batch = True

    try:  # get member from DB
        member = C3sMember.get_by_id(member_id)
        assert (member is not None)
    except AssertionError:
        if not batch:
            request.session.flash(
                "member with id {} not found in DB!".format(member_id),
                'message_to_staff')
            return HTTPFound(request.route_url('toolbox'))

    # sanity check:is this a member?
    try:
        assert (member.membership_accepted)  # must be accepted member!
    except AssertionError:
        request.session.flash(
            "member {} not accepted by the board!".format(member_id),
            'message_to_staff')
        return HTTPFound(request.route_url('toolbox'))

    if 'normal' not in member.membership_type and \
            'investing' not in member.membership_type:
        request.session.flash(
            'The membership type of member {0} is not specified! The '
            'membership type must either be "normal" or "investing" in order '
            'to be able to send an invoice email.'.format(member.id),
            'message_to_staff')
        return get_memberhip_listing_redirect(request)
    if member.membership_date >= date(2018,1,1) or ( \
                member.membership_loss_date is not None
                and
                member.membership_loss_date < date(2017,1,1)
            ):
        request.session.flash(
            'Member {0} was not a member in 2017. Therefore, you cannot send '
            'an invoice for 2017.'.format(member.id), 'message_to_staff')
        return get_memberhip_listing_redirect(request)

    # check if invoice no already exists.
    #     if yes: just send that email again!
    #     also: offer staffers to cancel this invoice

    if member.dues17_invoice is True:
        invoice = Dues17Invoice.get_by_invoice_no(member.dues17_invoice_no)
        member.dues17_invoice_date = datetime.now()

    else:  # if no invoice already exists:
        # make dues token and ...
        randomstring = make_random_string()
        # check if dues token is already used
        while (Dues17Invoice.check_for_existing_dues17_token(randomstring)):
            # create a new one, if the new one already exists in the database
            randomstring = make_random_string()  # pragma: no cover

        # prepare invoice number
        try:
            # either we already have an invoice number for that client...
            invoice_no = member.dues17_invoice_no
            assert invoice_no is not None
        except AssertionError:
            # ... or we create a new one and save it
            # get max invoice no from db
            max_invoice_no = Dues17Invoice.get_max_invoice_no()
            # use the next free number, save it to db
            new_invoice_no = int(max_invoice_no) + 1
            DBSession.flush()  # save dataset to DB

        # calculate dues amount (maybe partial, depending on quarter)
        dues_start, dues_amount = calculate_partial_dues17(member)

        # now we have enough info to update the member info
        # and persist invoice info for bookkeeping
        # store some info in DB/member table
        member.dues17_invoice = True
        member.dues17_invoice_no = new_invoice_no  # irrelevant for investing
        member.dues17_invoice_date = datetime.now()
        member.dues17_token = randomstring
        member.dues17_start = dues_start

        if 'normal' in member.membership_type:  # only for normal members
            member.set_dues17_amount(dues_amount)
            # store some more info about invoice in invoice table
            invoice = Dues17Invoice(
                invoice_no=member.dues17_invoice_no,
                invoice_no_string=(u'C3S-dues2017-' +
                                   str(member.dues17_invoice_no).zfill(4)),
                invoice_date=member.dues17_invoice_date,
                invoice_amount=u'' + str(member.dues17_amount),
                member_id=member.id,
                membership_no=member.membership_number,
                email=member.email,
                token=member.dues17_token,
            )
            DBSession.add(invoice)
        DBSession.flush()

    # now: prepare that email
    # only normal (not investing) members *have to* pay the dues.
    # only the normal members get an invoice link and PDF produced for them.
    # only investing legalentities are asked for more support.
    if 'investing' not in member.membership_type:
        start_quarter = string_start_quarter_dues17(member)
        invoice_url = (request.route_url(
            'make_dues17_invoice_no_pdf',
            email=member.email,
            code=member.dues17_token,
            i=str(member.dues17_invoice_no).zfill(4)))
        email_subject, email_body = make_dues17_invoice_email(
            member, invoice, invoice_url, start_quarter)
        message = Message(subject=email_subject,
                          sender='*****@*****.**',
                          recipients=[member.email],
                          body=email_body,
                          extra_headers={
                              'Reply-To': '*****@*****.**',
                          })
    elif 'investing' in member.membership_type:
        if member.is_legalentity:
            email_subject, email_body = \
                make_dues_invoice_legalentity_email(member)
        else:
            email_subject, email_body = \
                make_dues_invoice_investing_email(member)
        message = Message(subject=email_subject,
                          sender='*****@*****.**',
                          recipients=[member.email],
                          body=email_body,
                          extra_headers={
                              'Reply-To': '*****@*****.**',
                          })

    # print to console or send mail
    if 'true' in request.registry.settings['testing.mail_to_console']:
        print(message.body.encode('utf-8'))  # pragma: no cover
    else:
        send_message(request, message)

    # now choose where to redirect
    if 'detail' in request.referrer:
        return HTTPFound(
            request.route_url('detail', memberid=member.id) + '#dues17')
    if 'toolbox' in request.referrer:
        return HTTPFound(request.route_url('toolbox'))
    else:
        return get_memberhip_listing_redirect(request, member.id)
Exemplo n.º 19
0
def batch_invite(request):
    """
    Batch invite n members at the same time.

    The number (n) is configurable, defaults to 5.
    The number can either be supplied in the URL
    or by posting a form with 'number' and 'submit to this view.

    === =====================================
    URL http://app:port/invite_batch/{number}
    === =====================================
    """
    try:  # how many to process?
        batch_count = int(request.matchdict['number'])
    except (ValueError, KeyError):
        batch_count = 5
    if 'submit' in request.POST:
        try:
            batch_count = int(request.POST['number'])
        except ValueError:
            batch_count = 5

    invitees = C3sMember.get_invitees(batch_count)

    if len(invitees) == 0:
        request.session.flash('no invitees left. all done!',
                              'message_to_staff')
        return HTTPFound(request.route_url('toolbox'))

    num_sent = 0
    ids_sent = []

    for member in invitees:
        # prepare a random token iff none is set
        if member.email_invite_token_bcgv17 is None:
            member.email_invite_token_bcgv17 = make_random_token()
        url = URL_PATTERN.format(
            ticketing_url=request.registry.settings['ticketing.url'],
            token=member.email_invite_token_bcgv17,
            email=member.email)

        LOG.info("mailing event invitation to to member id %s", member.id)

        email_subject, email_body = make_bcga17_invitation_email(member, url)
        message = Message(subject=email_subject,
                          sender='*****@*****.**',
                          recipients=[member.email],
                          body=email_body,
                          extra_headers={
                              'Reply-To': '*****@*****.**',
                          })
        send_message(request, message)

        member.email_invite_flag_bcgv17 = True
        member.email_invite_date_bcgv17 = datetime.now()
        num_sent += 1
        ids_sent.append(member.id)

    request.session.flash(
        "sent out {} mails (to members with ids {})".format(
            num_sent, ids_sent), 'message_to_staff')

    return HTTPFound(request.route_url('toolbox'))