def dues15_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", 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') # 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'])), 'dues15_message_to_staff' # message queue for user ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') if DEBUG: print("DEBUG: member.dues15_amount: {}".format( member.dues15_amount)) print("DEBUG: type(member.dues15_amount): {}".format( type(member.dues15_amount))) print("DEBUG: member.dues15_reduced: {}".format( member.dues15_reduced)) print("DEBUG: member.dues15_amount_reduced: {}".format( member.dues15_amount_reduced)) print("DEBUG: type(member.dues15_amount_reduced): {}".format( type(member.dues15_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.', 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') # check the reduction amount: same as default calculated amount? if ((member.dues15_reduced is False) and ( member.dues15_amount == reduced_amount)): request.session.flash( u"Dieser Beitrag ist der default-Beitrag!", 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') if reduced_amount == member.dues15_amount_reduced: request.session.flash( u"Auf diesen Beitrag wurde schon reduziert!", 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') if member.dues15_reduced \ and reduced_amount > member.dues15_amount_reduced \ or reduced_amount > member.dues15_amount: request.session.flash( u'Beitrag darf nicht über den berechneten oder bereits' u'reduzierten Wert gesetzt werden.', 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') # prepare: get highest invoice no from db max_invoice_no = Dues15Invoice.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_dues15_reduced_amount(reduced_amount) request.session.flash('reduction to {}'.format(reduced_amount), 'dues15_message_to_staff') old_invoice = Dues15Invoice.get_by_invoice_no(member.dues15_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 = Dues15Invoice( invoice_no=new_invoice_no, invoice_no_string=( u'C3S-dues2015-' + 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.dues15_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 = Dues15Invoice( invoice_no=new_invoice_no + 1, invoice_no_string=( u'C3S-dues2015-' + 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.dues15_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.dues15_invoice_no = new_invoice_no + 1 DBSession.flush() # persist newer invoices reversal_url = ( request.route_url( 'make_dues15_reversal_invoice_pdf', email=member.email, code=member.dues15_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_dues15_invoice_no_pdf', email=member.email, code=member.dues15_token, i=str(new_invoice_no + 1).zfill(4) ) ) email_subject, email_body = make_dues_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!', 'dues15_message_to_staff') else: request.session.flash('update email was sent to user!', 'dues15_message_to_staff') send_message(request, message) return HTTPFound( request.route_url( 'detail', memberid=member_id) + '#dues15')
def dues15_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", 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') # 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'])), 'dues15_message_to_staff' # message queue for user ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') if DEBUG: print("DEBUG: member.dues15_amount: {}".format(member.dues15_amount)) print("DEBUG: type(member.dues15_amount): {}".format( type(member.dues15_amount))) print("DEBUG: member.dues15_reduced: {}".format(member.dues15_reduced)) print("DEBUG: member.dues15_amount_reduced: {}".format( member.dues15_amount_reduced)) print("DEBUG: type(member.dues15_amount_reduced): {}".format( type(member.dues15_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.', 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') # check the reduction amount: same as default calculated amount? if ((member.dues15_reduced is False) and (member.dues15_amount == reduced_amount)): request.session.flash( u"Dieser Beitrag ist der default-Beitrag!", 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') if reduced_amount == member.dues15_amount_reduced: request.session.flash( u"Auf diesen Beitrag wurde schon reduziert!", 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') if member.dues15_reduced \ and reduced_amount > member.dues15_amount_reduced \ or reduced_amount > member.dues15_amount: request.session.flash( u'Beitrag darf nicht über den berechneten oder bereits' u'reduzierten Wert gesetzt werden.', 'dues15_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues15') # prepare: get highest invoice no from db max_invoice_no = Dues15Invoice.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_dues15_reduced_amount(reduced_amount) request.session.flash('reduction to {}'.format(reduced_amount), 'dues15_message_to_staff') old_invoice = Dues15Invoice.get_by_invoice_no(member.dues15_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 = Dues15Invoice( invoice_no=new_invoice_no, invoice_no_string=(u'C3S-dues2015-' + 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.dues15_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 = Dues15Invoice( invoice_no=new_invoice_no + 1, invoice_no_string=(u'C3S-dues2015-' + 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.dues15_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.dues15_invoice_no = new_invoice_no + 1 DBSession.flush() # persist newer invoices reversal_url = (request.route_url( 'make_dues15_reversal_invoice_pdf', email=member.email, code=member.dues15_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_dues15_invoice_no_pdf', email=member.email, code=member.dues15_token, i=str(new_invoice_no + 1).zfill(4))) email_subject, email_body = make_dues_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!', 'dues15_message_to_staff') else: request.session.flash('update email was sent to user!', 'dues15_message_to_staff') send_message(request, message) return HTTPFound( request.route_url('detail', memberid=member_id) + '#dues15')
def send_dues15_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(2016,1,1): request.session.flash( 'Member {0} was not a member in 2015. Therefore, you cannot send ' 'an invoice for 2015.'.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.dues15_invoice is True: invoice = Dues15Invoice.get_by_invoice_no(member.dues15_invoice_no) member.dues15_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 (Dues15Invoice.check_for_existing_dues15_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.dues15_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 = Dues15Invoice.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_dues15(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.dues15_invoice = True member.dues15_invoice_no = new_invoice_no # irrelevant for investing member.dues15_invoice_date = datetime.now() member.dues15_token = randomstring member.dues15_start = dues_start if 'normal' in member.membership_type: # only for normal members member.set_dues15_amount(dues_amount) # store some more info about invoice in invoice table invoice = Dues15Invoice( invoice_no=member.dues15_invoice_no, invoice_no_string=( u'C3S-dues2015-' + str(member.dues15_invoice_no).zfill(4)), invoice_date=member.dues15_invoice_date, invoice_amount=u'' + str(member.dues15_amount), member_id=member.id, membership_no=member.membership_number, email=member.email, token=member.dues15_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 'normal' in member.membership_type: start_quarter = string_start_quarter(member) invoice_url = ( request.route_url( 'make_dues15_invoice_no_pdf', email=member.email, code=member.dues15_token, i=str(member.dues15_invoice_no).zfill(4) ) ) email_subject, email_body = make_dues_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) + '#dues15') if 'toolbox' in request.referrer: return HTTPFound(request.route_url('toolbox')) else: return get_memberhip_listing_redirect(request, member.id)
def send_dues15_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(2016, 1, 1): request.session.flash( 'Member {0} was not a member in 2015. Therefore, you cannot send ' 'an invoice for 2015.'.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.dues15_invoice is True: invoice = Dues15Invoice.get_by_invoice_no(member.dues15_invoice_no) member.dues15_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 (Dues15Invoice.check_for_existing_dues15_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.dues15_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 = Dues15Invoice.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_dues15(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.dues15_invoice = True member.dues15_invoice_no = new_invoice_no # irrelevant for investing member.dues15_invoice_date = datetime.now() member.dues15_token = randomstring member.dues15_start = dues_start if 'normal' in member.membership_type: # only for normal members member.set_dues15_amount(dues_amount) # store some more info about invoice in invoice table invoice = Dues15Invoice( invoice_no=member.dues15_invoice_no, invoice_no_string=(u'C3S-dues2015-' + str(member.dues15_invoice_no).zfill(4)), invoice_date=member.dues15_invoice_date, invoice_amount=u'' + str(member.dues15_amount), member_id=member.id, membership_no=member.membership_number, email=member.email, token=member.dues15_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 'normal' in member.membership_type: start_quarter = string_start_quarter(member) invoice_url = (request.route_url( 'make_dues15_invoice_no_pdf', email=member.email, code=member.dues15_token, i=str(member.dues15_invoice_no).zfill(4))) email_subject, email_body = make_dues_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) + '#dues15') if 'toolbox' in request.referrer: return HTTPFound(request.route_url('toolbox')) else: return get_memberhip_listing_redirect(request, member.id)