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)
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()
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)
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)
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 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)
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()
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)
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)
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)
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)
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)
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')
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)
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'))