def get_pickup_dates(start, stop, month_start=False): if isinstance(start, datetime.date): start = pytz.utc.localize( datetime.datetime(start.year, start.month, start.day, 12) ) bw = get_billing_week(start) if month_start: while bw.week != 1: next_start = bw.start - timedelta(days=6, hours=1) bw = get_billing_week(next_start) counter = 1 result = OrderedDict() result[datetime.date(bw.year, bw.month, 1)] = [bw] while counter < 1000: if isinstance(stop, int): if counter == stop: return result else: if bw.start > stop: return result bw = get_billing_week(bw.end + timedelta(hours=1)) d = datetime.date(bw.year, bw.month, 1) if d not in result: result[d] = [] result[d].append(bw) counter += 1 raise Exception( 'Found more than 1000 or so pickup dates, perhaps there is a problem?' )
def pickup_list(self, request, queryset): now = timezone.now() bw = get_billing_week(now) initial = { 'billing_week': str(bw) } if '_generate' in request.POST: form = pickupListForm(request.POST, initial=initial) if form.is_valid(): context = pickup_list( queryset, parse_billing_week(form.cleaned_data['billing_week']) ) context['now_billing_week'] = bw # context['now_billing_week'] = context['billing_week'] context['now'] = now return render(request, 'pickup-list.html', context) else: form = pickupListForm(initial=initial) return render( request, 'pickup-list-date.html', { 'form': form, 'post_vars': request.POST.lists(), 'opts': CollectionPoint._meta, 'change': True, 'is_popup': False, 'save_as': False, 'has_delete_permission': False, 'has_add_permission': False, 'has_change_permission': False, } )
def _set_bag_quantities(self, bag_quantities, reason='CHANGE'): now = timezone.now() bw = get_billing_week(now) customer_order_change = CustomerOrderChange( changed=now, changed_in_billing_week=str(bw), customer=self, reason=reason, ) customer_order_change.save() for bag_type, quantity in bag_quantities.items(): if not isinstance(bag_type, BagType): bag_quantity = CustomerOrderChangeBagQuantity( customer_order_change=customer_order_change, bag_type_id=int(bag_type), quantity=quantity, ) else: bag_quantity = CustomerOrderChangeBagQuantity( customer_order_change=customer_order_change, bag_type=bag_type, quantity=quantity, ) # XXX Do we need the save? bag_quantity.save()
def save_model(self, request, line_item, form, change): now = timezone.now() bw = get_billing_week(now) line_item.reason = LineItem.MANUAL line_item.created = now line_item.created_in_billing_week = str(bw) line_item.save()
def pickup_list(self, request, queryset): now = timezone.now() bw = get_billing_week(now) initial = {'billing_week': str(bw)} if '_generate' in request.POST: form = pickupListForm(request.POST, initial=initial) if form.is_valid(): context = pickup_list( queryset, parse_billing_week(form.cleaned_data['billing_week'])) context['now_billing_week'] = bw # context['now_billing_week'] = context['billing_week'] context['now'] = now return render(request, 'pickup-list.html', context) else: form = pickupListForm(initial=initial) return render( request, 'pickup-list-date.html', { 'form': form, 'post_vars': request.POST.lists(), 'opts': CollectionPoint._meta, 'change': True, 'is_popup': False, 'save_as': False, 'has_delete_permission': False, 'has_add_permission': False, 'has_change_permission': False, })
def _get_skipped(self): skipped_dates = [] now = timezone.now() bw = get_billing_week(now) bwstr = str(bw) if Skip.objects.filter(customer=self, billing_week=str(bwstr)).all(): return True return False
def create_now(self, **p): assert 'created' not in p assert 'created_in_billing_week' not in p if 'now' not in p: p['now'] = timezone.now() now = p['now'] del p['now'] p['created'] = now p['created_in_billing_week'] = str(get_billing_week(now)) return self.create(**p)
def gocardless_events_webhook(request): """ POST and GET are empty: <QueryDict: {}> Body is a list of events: e.g. {"events":[{"id":"EVTESTWZXEMZMJ","created_at":"2016-08-08T13:33:07.528Z","resource_type":"payments","action":"created","links":{"payment":"index_ID_123"},"details":{"origin":"api","cause":"payment_created","description":"Payment created via the API."},"metadata":{}}]} """ print('Here!') # Documentation is: https://developer.gocardless.com/2015-07-06/#webhooks-examples # 1. Check the signature dig = hmac.new(settings.GOCARDLESS_WEBHOOK_SECRET.encode('utf8'), msg=request.body, digestmod=hashlib.sha256).hexdigest() if request.META['HTTP_WEBHOOK_SIGNATURE'] != dig: print(request.META['HTTP_WEBHOOK_SIGNATURE'], '!=', dig) return HttpResponse('Invalid Token', status=498, reason='Invalid Token') data = request.body.decode('utf-8') print(data) now = timezone.now() bw = get_billing_week(now) payload = json.loads(data) for event in payload['events']: # 2. Check that you have not already processed this event when # receiving the same webhook for a different webhook endpoint if len(list( GoCardlessEvent.objects.filter(event_id=event['id']))) != 0: print('Already got this event: {}'.format(event)) continue webhook = GoCardlessEvent(event=event, event_id=event['id']) webhook.save() # 3. Fetch the updated resource, using the ID supplied, and check that # it has not changed further since the webhook was sent (since # webhooks may arrive out of order) if event["resource_type"] == "payments": payment_id = event['links']['payment'] payment_response = settings.GOCARDLESS_CLIENT.payments.get( payment_id) # 4. Act on the event, e.g. shipping goods, extending subscription payment = Payment.objects.filter( gocardless_payment_id=payment_id).get() payment_status_change = PaymentStatusChange( changed=now, changed_in_billing_week=str(bw), status=payment_response.status, payment=payment, ) payment_status_change.save() # Let's treat 'confirmed' as paid, 'paid_out' is when we receive the payment if payment_response.status == 'confirmed': # XXX Is this what we want? payment.completed = now payment.completed_in_billing_week = bw payment.save() # XXX Can send email alerts for other conditions? return HttpResponse('ok', status=200, reason='OK')
def dashboard_leave(request): if request.method == 'POST' and request.POST.get('cancel'): messages.add_message( request, messages.INFO, 'You are still part of the scheme, and haven\'t left.') return redirect(reverse("dashboard")) # if this is a POST request we need to process the form data if request.method == 'POST': # create a form instance and populate it with data from the request: form = LeaveReasonForm(request.POST) # check whether it's valid: if form.is_valid(): reason = dict( form.fields['reason'].choices)[form.cleaned_data['reason']] a = send_mail( '[BlueWorld] Leaver Notification', ''' Hello from BlueWorldLite, {} has decided to leave the scheme. Here are the details: Reason: {}\n Comments: {} Thanks, The BlueWorldLite system '''.format( request.user.customer.full_name, reason, form.cleaned_data['comments'], ), settings.DEFAULT_FROM_EMAIL, settings.LEAVER_EMAIL_TO, fail_silently=False, ) # Only save changes now in case there is a problem with the email now = timezone.now() bw = get_billing_week(now) account_status_change = AccountStatusChange( changed=now, changed_in_billing_week=str(bw), customer=request.user.customer, status=AccountStatusChange.LEFT, ) account_status_change.save() return redirect(reverse('dashboard_bye')) # if a GET (or any other method) we'll create a blank form else: form = LeaveReasonForm() return render(request, 'dashboard/leave.html', { 'form': form, })
def handle(self, *args, **options): json_path = options['path'] self.stdout.write(self.style.NOTICE("looking up this path: {}".format(json_path))) with open(json_path) as data_file: blob_o_stuff = json.load(data_file) customers = [item for item in blob_o_stuff if item['model'] == 'customers.customer'] bag_choices = [item for item in blob_o_stuff if item['model'] == 'customers.bagchoice'] gc_subs = [item for item in blob_o_stuff if item['model'] == 'customers.gcsubscription'] self.stdout.write(self.style.NOTICE("Found {} customer entries".format(len(customers)))) self.stdout.write(self.style.NOTICE("Found {} bagchoice entries".format(len(bag_choices)))) self.stdout.write(self.style.NOTICE("Found {} gc_sub entries".format(len(gc_subs)))) total_customers = len(customers) for (index, c) in enumerate(customers): user = self._make_user(c) now = timezone.now() bw = get_billing_week(now) cf = c['fields'] customer = Customer( created=now, created_in_billing_week=str(bw), full_name="{} {}".format(cf['first_name'], cf['surname']), nickname=cf['first_name'], mobile=cf['telephone_1'], user=user, id=c['pk'] ) customer.save() account_status_change = AccountStatusChange( changed=now, changed_in_billing_week=str(bw), customer=customer, status=AccountStatusChange.ACTIVE, ) account_status_change.save() old_bag_choices = [b for b in bag_choices if b['fields']['customer'] == c['pk']] customer.bag_quantities = self._convert_bag_choices(old_bag_choices) customer.collection_point = cf['pickup'] self.stdout.write(self.style.SUCCESS("Imported {} of {}: {}".format( index, total_customers, customer.full_name)))
def _set_collection_point(self, collection_point_id): if not isinstance(collection_point_id, int): collection_point_id = collection_point_id.id now = timezone.now() bw = get_billing_week(now) collection_point_change = CustomerCollectionPointChange( changed=now, changed_in_billing_week=str(bw), customer=self, collection_point_id=collection_point_id, ) collection_point_change.save()
def dashboard_gocardless(request): now = timezone.now() bw = get_billing_week(now) if request.method == 'POST': assert request.user.username # from django.contrib.sites.models import Site # current_site = Site.objects.get_current() # success_redirect_url = 'http://{}{}'.format( # current_site.domain, # reverse('gocardless_callback'), # ) session_token = str(uuid.uuid4()) success_redirect_url = '{}://{}{}'.format( request.scheme, request.META['HTTP_HOST'], reverse('gocardless_callback'), ) if not settings.SKIP_GOCARDLESS: redirect_flow = settings.GOCARDLESS_CLIENT.redirect_flows.create(params={ 'description': 'Your Growing Communities Veg Box Order', 'session_token': session_token, 'success_redirect_url': success_redirect_url, # 'scheme': ... DD scheme. }) else: redirect_flow = _RF() redirect_flow.id = str(uuid.uuid4()) mandate = BillingGoCardlessMandate( session_token=session_token, gocardless_redirect_flow_id=redirect_flow.id, customer=request.user.customer, created=now, created_in_billing_week=str(bw), amount_notified=Decimal(request.POST['amount_notified']), ) mandate.save() if settings.SKIP_GOCARDLESS: return redirect(reverse('gocardless_callback')+'?skip=true') else: return redirect(redirect_flow.redirect_url) else: number, amount, amount_per_week, first_bw = _get_cost_for_billing_week(request.user.customer, bw) return render( request, 'dashboard/set-up-go-cardless.html', { 'amount': amount, 'number': number, 'first_bw': first_bw, } )
def _set_weekly_cost(self, weekly_cost): assert isinstance(weekly_cost, Decimal), \ 'Expecting a decimal, not {!r}'.format(weekly_cost) # XXX Potential race condition here cost_changes = BagTypeCostChange.objects.order_by('-changed').filter( bag_type=self)[:1] if not len(cost_changes) or cost_changes[0].weekly_cost != weekly_cost: now = timezone.now() bw = get_billing_week(now) cost_change = BagTypeCostChange(changed=now, changed_in_billing_week=str(bw), weekly_cost=weekly_cost, bag_type=self) cost_change.save()
def dashboard_gocardless(request): now = timezone.now() bw = get_billing_week(now) if request.method == 'POST': assert request.user.username # from django.contrib.sites.models import Site # current_site = Site.objects.get_current() # success_redirect_url = 'http://{}{}'.format( # current_site.domain, # reverse('gocardless_callback'), # ) session_token = str(uuid.uuid4()) success_redirect_url = '{}://{}{}'.format( request.scheme, request.META['HTTP_HOST'], reverse('gocardless_callback'), ) if not settings.SKIP_GOCARDLESS: redirect_flow = settings.GOCARDLESS_CLIENT.redirect_flows.create( params={ 'description': 'Your Growing Communities Veg Box Order', 'session_token': session_token, 'success_redirect_url': success_redirect_url, # 'scheme': ... DD scheme. }) else: redirect_flow = _RF() redirect_flow.id = str(uuid.uuid4()) mandate = BillingGoCardlessMandate( session_token=session_token, gocardless_redirect_flow_id=redirect_flow.id, customer=request.user.customer, created=now, created_in_billing_week=str(bw), amount_notified=Decimal(request.POST['amount_notified']), ) mandate.save() if settings.SKIP_GOCARDLESS: return redirect(reverse('gocardless_callback') + '?skip=true') else: return redirect(redirect_flow.redirect_url) else: number, amount, amount_per_week, first_bw = _get_cost_for_billing_week( request.user.customer, bw) return render(request, 'dashboard/set-up-go-cardless.html', { 'amount': amount, 'number': number, 'first_bw': first_bw, })
def queryset(self, request, queryset): if self.value() is None: return queryset # Check for people on holiday if self.value() == "HOLIDAY": # TODO check if we should move this into a separate filter customer_ids = Skip.objects.filter(billing_week=get_billing_week( timezone.now())).only('customer_id').values_list('customer_id') else: # otherwise customer_ids = self._by_status(self.value()) return queryset.filter(pk__in=customer_ids)
def queryset(self, request, queryset): if self.value() is None: return queryset # Check for people on holiday if self.value() == "HOLIDAY": # TODO check if we should move this into a separate filter customer_ids = Skip.objects.filter( billing_week=get_billing_week(timezone.now()) ).only('customer_id').values_list('customer_id') else: # otherwise customer_ids = self._by_status(self.value()) return queryset.filter(pk__in=customer_ids)
def gocardless_events_webhook(request): """ POST and GET are empty: <QueryDict: {}> Body is a list of events: e.g. {"events":[{"id":"EVTESTWZXEMZMJ","created_at":"2016-08-08T13:33:07.528Z","resource_type":"payments","action":"created","links":{"payment":"index_ID_123"},"details":{"origin":"api","cause":"payment_created","description":"Payment created via the API."},"metadata":{}}]} """ print('Here!') # Documentation is: https://developer.gocardless.com/2015-07-06/#webhooks-examples # 1. Check the signature dig = hmac.new(settings.GOCARDLESS_WEBHOOK_SECRET.encode('utf8'), msg=request.body, digestmod=hashlib.sha256).hexdigest() if request.META['HTTP_WEBHOOK_SIGNATURE'] != dig: print(request.META['HTTP_WEBHOOK_SIGNATURE'], '!=', dig) return HttpResponse('Invalid Token', status=498, reason='Invalid Token') data = request.body.decode('utf-8') print(data) now = timezone.now() bw = get_billing_week(now) payload = json.loads(data) for event in payload['events']: # 2. Check that you have not already processed this event when # receiving the same webhook for a different webhook endpoint if len(list(GoCardlessEvent.objects.filter(event_id=event['id']))) != 0: print('Already got this event: {}'.format(event)) continue webhook = GoCardlessEvent(event=event, event_id=event['id']) webhook.save() # 3. Fetch the updated resource, using the ID supplied, and check that # it has not changed further since the webhook was sent (since # webhooks may arrive out of order) if event["resource_type"] == "payments": payment_id = event['links']['payment'] payment_response = settings.GOCARDLESS_CLIENT.payments.get(payment_id) # 4. Act on the event, e.g. shipping goods, extending subscription payment = Payment.objects.filter(gocardless_payment_id=payment_id).get() payment_status_change = PaymentStatusChange( changed=now, changed_in_billing_week=str(bw), status=payment_response.status, payment=payment, ) payment_status_change.save() # Let's treat 'confirmed' as paid, 'paid_out' is when we receive the payment if payment_response.status == 'confirmed': # XXX Is this what we want? payment.completed = now payment.completed_in_billing_week = bw payment.save() # XXX Can send email alerts for other conditions? return HttpResponse('ok', status=200, reason='OK')
def billing_dates(request): pickup_dates = get_pickup_dates(start_of_the_month(timezone.now().year, timezone.now().month), 52, month_start=True) billing_dates = OrderedDict() bw_today = get_billing_week(timezone.now()) for month in pickup_dates: billing_dates[month] = pickup_dates[month][0].start return render( request, 'billing-dates.html', { 'pickup_dates': pickup_dates, 'billing_dates': billing_dates, 'billing_weeks_left': billing_weeks_left_in_the_month( str(bw_today)), 'current_billing_week': bw_today })
def _set_weekly_cost(self, weekly_cost): assert isinstance(weekly_cost, Decimal), \ 'Expecting a decimal, not {!r}'.format(weekly_cost) # XXX Potential race condition here cost_changes = BagTypeCostChange.objects.order_by( '-changed' ).filter(bag_type=self)[:1] if not len(cost_changes) or cost_changes[0].weekly_cost != weekly_cost: now = timezone.now() bw = get_billing_week(now) cost_change = BagTypeCostChange( changed=now, changed_in_billing_week=str(bw), weekly_cost=weekly_cost, bag_type=self ) cost_change.save()
def dashboard_rejoin_scheme(request): if request.method == 'POST': now = timezone.now() bw = get_billing_week(now) account_status_change = AccountStatusChange( changed=now, changed_in_billing_week=str(bw), customer=request.user.customer, status=AccountStatusChange.ACTIVE, ) account_status_change.save() messages.add_message( request, messages.INFO, 'Successfully re-activated your account', ) return redirect(reverse("dashboard")) else: return HttpResponseBadRequest()
def billing_dates(request): pickup_dates = get_pickup_dates( start_of_the_month(timezone.now().year, timezone.now().month), 52, month_start=True ) billing_dates = OrderedDict() bw_today = get_billing_week(timezone.now()) for month in pickup_dates: billing_dates[month] = pickup_dates[month][0].start return render( request, 'billing-dates.html', { 'pickup_dates': pickup_dates, 'billing_dates': billing_dates, 'billing_weeks_left': billing_weeks_left_in_the_month(str(bw_today)), 'current_billing_week': bw_today } )
def save_model(self, request, payment, form, change): mandate_id = payment.customer.gocardless_current_mandate.gocardless_mandate_id if not settings.SKIP_GOCARDLESS: payment_response_id, payment_response_status = payment.send_to_gocardless(mandate_id, payment.amount) else: payment_response_id = 'none' payment_response_status = 'skipped' now = timezone.now() bw = get_billing_week(now) payment.created = now payment.created_in_billing_week = str(bw) payment.gocardless_mandate_id = mandate_id payment.gocardless_payment_id = payment_response_id payment.reason = Payment.MANUAL payment.save() payment_status_change = PaymentStatusChange( changed=now, changed_in_billing_week=str(bw), status=payment_response_status, payment=payment, ) payment_status_change.save()
def signup(self, request, user): now = timezone.now() bw = get_billing_week(now) customer = Customer( created=now, created_in_billing_week=str(bw), full_name=self.cleaned_data['full_name'], nickname=self.cleaned_data['nickname'], mobile=self.cleaned_data['mobile'], user=user, ) customer.save() account_status_change = AccountStatusChange( changed=now, changed_in_billing_week=str(bw), customer=customer, status=AccountStatusChange.AWAITING_DIRECT_DEBIT, ) account_status_change.save() customer.collection_point = request.session['collection_point'] customer._set_bag_quantities(request.session['bag_type'], reason='JOIN')
def save_model(self, request, payment, form, change): mandate_id = payment.customer.gocardless_current_mandate.gocardless_mandate_id if not settings.SKIP_GOCARDLESS: payment_response_id, payment_response_status = payment.send_to_gocardless( mandate_id, payment.amount) else: payment_response_id = 'none' payment_response_status = 'skipped' now = timezone.now() bw = get_billing_week(now) payment.created = now payment.created_in_billing_week = str(bw) payment.gocardless_mandate_id = mandate_id payment.gocardless_payment_id = payment_response_id payment.reason = Payment.MANUAL payment.save() payment_status_change = PaymentStatusChange( changed=now, changed_in_billing_week=str(bw), status=payment_response_status, payment=payment, ) payment_status_change.save()
def dashboard_skip_weeks(request): now = timezone.now() bw = get_billing_week(now) if request.method == 'POST' and request.POST.get('cancel'): messages.add_message(request, messages.INFO, 'Your skip weeks have not been changed.') return redirect(reverse("dashboard")) SkipFormSet = formset_factory( SkipForm, extra=0, formset=BaseSkipFormSet, ) # We want to see the next 9 skip weeks we can change # We can't change things in the current billing week but # but we can change things in the next billing week skipped_billing_weeks = [] if settings.ALLOW_SKIP_CURRENT_WEEK: startbw = bw bwqstr = str(bw) else: startbw = bw.next() bwqstr = str(startbw) for skip in Skip.objects.order_by('billing_week').filter( customer=request.user.customer, billing_week__gte=bwqstr, ): skipped_billing_weeks.append(skip.billing_week) valid_dates = {} initial_skips = [] # Offer dates this month and for the next two. for month, pickup_dates in get_pickup_dates(startbw.wed, 9).items(): for skipbw in pickup_dates: pickup_date = skipbw.wed skip_choice = { 'id': str(skipbw), 'skipbw': skipbw, 'skipped': str(skipbw) in skipped_billing_weeks, } initial_skips.append(skip_choice) valid_dates[str(skipbw)] = skip_choice if request.method == 'POST': formset = SkipFormSet( request.POST, request.FILES, initial=initial_skips, ) if formset.is_valid(): to_skip = [] to_unskip = [] for row in formset.cleaned_data: skipbw = row['id'] if row['skipped'] != valid_dates[skipbw]['skipped']: if row['skipped'] is True: to_skip.append(skipbw) else: to_unskip.append(skipbw) if not to_skip and not to_unskip: messages.add_message( request, messages.ERROR, ''' You haven't made any changes to your skip weeks. You can click Cancel if you are happy with your skip weeks as they are. ''') return redirect(reverse("dashboard_skip_weeks")) for skipbw in to_skip: assert skipbw not in to_unskip skip = Skip( created=now, created_in_billing_week=str(bw), billing_week=skipbw, customer=request.user.customer, ) skip.save() for skipbw in to_unskip: skip = Skip.objects.filter(customer=request.user.customer, billing_week=skipbw).get() skip.delete() messages.add_message( request, messages.SUCCESS, 'Your skip weeks have been updated successfully') return redirect(reverse("dashboard")) else: formset = SkipFormSet(initial=initial_skips) return render(request, 'dashboard/skip-weeks.html', {'formset': formset})
def gocardless_callback(request): if settings.GOCARDLESS_ENVIRONMENT == 'sandbox' and \ request.GET.get('skip', '').lower() == 'true': assert settings.SKIP_GOCARDLESS mandate = list( BillingGoCardlessMandate.objects.filter( customer=request.user.customer, ).all())[-1] mandate.gocardless_mandate_id = str(uuid.uuid4()) else: mandate = BillingGoCardlessMandate.objects.filter( customer=request.user.customer, gocardless_redirect_flow_id=request.GET['redirect_flow_id'], ).get() complete_redirect_flow = settings.GOCARDLESS_CLIENT.redirect_flows.complete( mandate.gocardless_redirect_flow_id, {'session_token': mandate.session_token}) assert complete_redirect_flow.id == mandate.gocardless_redirect_flow_id mandate.gocardless_mandate_id = complete_redirect_flow.links.mandate now = timezone.now() bw = get_billing_week(now) mandate.in_use_for_customer = request.user.customer mandate.completed = now mandate.completed_in_billing_week = str(bw) mandate.save() tags = CustomerTag.objects.filter(tag='Starter').all() if tags: request.user.customer.tags.add(tags[0]) request.user.customer.save() # Now, take the first payment if it is needed # Get the amount and date from the session. number, amount_pounds, amount_per_week, first_bw_of_next_month = _get_cost_for_billing_week( request.user.customer, bw) # If this isn't true, it is because it took over a week to complete GoCardless and this has to be dealt with manually. assert amount_pounds <= mandate.amount_notified assert int(amount_pounds * 100) == (amount_pounds * 100) if amount_pounds: # We don't want to make a payment unless there is something to pay. mandate_id = request.user.customer.gocardless_current_mandate.gocardless_mandate_id if settings.SKIP_GOCARDLESS: payment_response_id = str(uuid.uuid4()) payment_response_status = 'skipped' else: payment_response_id, payment_response_status = Payment.send_to_gocardless( mandate_id, amount_pounds) payment = Payment( customer=request.user.customer, gocardless_mandate_id=mandate_id, amount=amount_pounds, reason=Payment.JOIN_WITH_COLLECTIONS_AVAILABLE, gocardless_payment_id=payment_response_id, created=now, created_in_billing_week=str(bw), ) payment.save() payment_status_change = PaymentStatusChange( changed=now, changed_in_billing_week=str(bw), status=payment_response_status, payment=payment, ) payment_status_change.save() bill_against = bw.next() for x in range(number): li = LineItem(payment=payment, created=now, created_in_billing_week=bw, bill_against=str(bill_against), customer=request.user.customer, amount=amount_per_week, reason=LineItem.NEW_JOINER) li.save() bill_against = bw.next() account_status_change = AccountStatusChange( changed=now, changed_in_billing_week=str(bw), customer=request.user.customer, status=AccountStatusChange.ACTIVE, ) account_status_change.save() messages.add_message(request, messages.INFO, 'Successfully set up Go Cardless.') return redirect(reverse("dashboard"))
def dashboard(request): if request.user.customer.account_status != AccountStatusChange.LEFT: latest_cp_change = CustomerCollectionPointChange.objects.order_by( '-changed').filter(customer=request.user.customer)[:1] latest_customer_order_change = CustomerOrderChange.objects.order_by( '-changed').filter(customer=request.user.customer)[:1] collection_point = latest_cp_change[0].collection_point now = timezone.now() bw = get_billing_week(now) weekday = now.weekday() skipped_billing_weeks = [] skipped = len( Skip.objects.order_by('billing_week').filter( customer=request.user.customer, billing_week=str(bw), ).all()) > 0 if weekday == 6: # Sunday if collection_point.collection_day == 'WED': collection_date = 'Wednesday' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday' else: collection_date = 'Wednesday and Thursday' if timezone.now().hour < bw.end.hour: deadline = '3pm today' changes_affect = "next week's collection" else: deadline = '3pm next Sunday' changes_affect = "the collection after next" elif weekday == 0: # Monday if collection_point.collection_day == 'WED': collection_date = 'Wednesday' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday' else: collection_date = 'Wednesday and Thursday' deadline = '3pm this Sunday' changes_affect = "next week's collection" elif weekday == 1: # Tuesday if collection_point.collection_day == 'WED': collection_date = 'tomorrow' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday' else: collection_date = 'tomorrow and Thursday' deadline = '3pm this Sunday' changes_affect = "next week's collection" elif weekday == 2: # Wednesday if collection_point.collection_day == 'WED': collection_date = 'today' elif collection_point.collection_day == 'THURS': collection_date = 'tomorrow' else: collection_date = 'today and tomorrow' deadline = '3pm this Sunday' changes_affect = "next week's collection" elif weekday == 3: # Thurs if collection_point.collection_day == 'WED': collection_date = 'Wednesday next week' elif collection_point.collection_day == 'THURS': collection_date = 'today' else: collection_date = 'today' deadline = '3pm this Sunday' changes_affect = "next week's collection" else: if collection_point.collection_day == 'WED': collection_date = 'Wednesday next week' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday next week' else: collection_date = 'Wednesday and Thursday next week' if weekday == 4: # Friday deadline = '3pm this Sunday' else: # Saturday deadline = '3pm tomorrow' changes_affect = "next week's collection" # fall back for the case when we have a user just starting this week if request.user.customer.created_in_billing_week == bw: if collection_point.collection_day == 'WED': collection_date = 'Wednesday next week' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday next week' else: collection_date = 'Wednesday and Thursday next week' if weekday == 4: # Friday deadline = '3pm this Sunday' else: # Saturday deadline = '3pm tomorrow' changes_affect = "next week's collection" bag_quantities = CustomerOrderChangeBagQuantity.objects.filter( customer_order_change=latest_customer_order_change).all() if skipped: collection_date = collection_date.replace(' and ', ' or ') if not (collection_date.startswith('today') or collection_date.startswith('tomorrow')): collection_date = 'on ' + collection_date return render( request, 'dashboard/index.html', { 'bag_quantities': bag_quantities, 'collection_point': collection_point, 'collection_date': collection_date, 'deadline': deadline, 'changes_affect': changes_affect, 'skipped': skipped, }) else: return render( request, 'dashboard/re-join-scheme.html', )
def create_payments(year, month, start_customer=0): now = timezone.now() # If the account is in credit after taking the line items into account, it will be ignored this month. # Upcoming line items can show on the billing history page. now_bw = get_billing_week(now) # Find the last deadline of the month billing_week = parse_billing_week('{0}-{1:02d} {2}'.format(year, month, 1)) assert now_bw >= billing_week, 'Cannot create payments in the future' last_run = PaymentRun.objects.order_by('-started')[:1] if last_run: assert last_run[0].started < now, 'The last run was in the future' payment_run = PaymentRun.objects.create( job_id = 'xxx', year = year, month = month, started = now, finished =None, start_customer = start_customer or None, currently_processing_customer = None, ) payment_run.save() payment_by_customer = {} # billing_weeks_this_month = [] # while billing_week.month == month: # billing_weeks_this_month.append(billing_week) # billing_week = billing_week.next() # print(billing_weeks_this_month) # For each customer for customer in Customer.objects.order_by('pk'): if customer.pk < start_customer: print('Skip') continue if not payment_run.start_customer: payment_run.start_customer = customer payment_run.currently_processing_customer = customer payment_run.save() print(customer) # Look at *all* line items that don't have a payment and create the payment object for them line_items = LineItem.objects.filter(payment=None, customer=customer) total = Decimal(0) for line_item in line_items: total += line_item.amount print(total) # If the account is in credit after taking the line items into account, it will be ignored this month. if total > 0: mandate_id = 'xxx', #customer.gocardless_current_mandate.gocardless_mandate_id payment_response_id = 'xxx' payment_response_status = 'xxx' payment = Payment( customer=customer, gocardless_mandate_id=mandate_id, amount=total, reason=Payment.MONTHLY_INVOICE, gocardless_payment_id=payment_response_id, created=now, created_in_billing_week=str(now_bw), ) payment.save() payment_status_change = PaymentStatusChange( changed=now, changed_in_billing_week=str(now_bw), status=payment_response_status, payment=payment, ) payment_status_change.save() for line_item in line_items: line_item.payment = payment line_item.save() payment_by_customer[customer] = payment return payment_by_customer
def create_payments(year, month, start_customer=0): now = timezone.now() # If the account is in credit after taking the line items into account, it will be ignored this month. # Upcoming line items can show on the billing history page. now_bw = get_billing_week(now) # Find the last deadline of the month billing_week = parse_billing_week('{0}-{1:02d} {2}'.format(year, month, 1)) assert now_bw >= billing_week, 'Cannot create payments in the future' last_run = PaymentRun.objects.order_by('-started')[:1] if last_run: assert last_run[0].started < now, 'The last run was in the future' payment_run = PaymentRun.objects.create( job_id='xxx', year=year, month=month, started=now, finished=None, start_customer=start_customer or None, currently_processing_customer=None, ) payment_run.save() payment_by_customer = {} # billing_weeks_this_month = [] # while billing_week.month == month: # billing_weeks_this_month.append(billing_week) # billing_week = billing_week.next() # print(billing_weeks_this_month) # For each customer for customer in Customer.objects.order_by('pk'): if customer.pk < start_customer: print('Skip') continue if not payment_run.start_customer: payment_run.start_customer = customer payment_run.currently_processing_customer = customer payment_run.save() print(customer) # Look at *all* line items that don't have a payment and create the payment object for them line_items = LineItem.objects.filter(payment=None, customer=customer) total = Decimal(0) for line_item in line_items: total += line_item.amount print(total) # If the account is in credit after taking the line items into account, it will be ignored this month. if total > 0: mandate_id = 'xxx', #customer.gocardless_current_mandate.gocardless_mandate_id payment_response_id = 'xxx' payment_response_status = 'xxx' payment = Payment( customer=customer, gocardless_mandate_id=mandate_id, amount=total, reason=Payment.MONTHLY_INVOICE, gocardless_payment_id=payment_response_id, created=now, created_in_billing_week=str(now_bw), ) payment.save() payment_status_change = PaymentStatusChange( changed=now, changed_in_billing_week=str(now_bw), status=payment_response_status, payment=payment, ) payment_status_change.save() for line_item in line_items: line_item.payment = payment line_item.save() payment_by_customer[customer] = payment return payment_by_customer
def dashboard_leave(request): if request.method == 'POST' and request.POST.get('cancel'): messages.add_message( request, messages.INFO, 'You are still part of the scheme, and haven\'t left.' ) return redirect(reverse("dashboard")) # if this is a POST request we need to process the form data if request.method == 'POST': # create a form instance and populate it with data from the request: form = LeaveReasonForm(request.POST) # check whether it's valid: if form.is_valid(): reason = dict(form.fields['reason'].choices)[ form.cleaned_data['reason'] ] a = send_mail( '[BlueWorld] Leaver Notification', ''' Hello from BlueWorldLite, {} has decided to leave the scheme. Here are the details: Reason: {}\n Comments: {} Thanks, The BlueWorldLite system '''.format( request.user.customer.full_name, reason, form.cleaned_data['comments'], ), settings.DEFAULT_FROM_EMAIL, settings.LEAVER_EMAIL_TO, fail_silently=False, ) # Only save changes now in case there is a problem with the email now = timezone.now() bw = get_billing_week(now) account_status_change = AccountStatusChange( changed=now, changed_in_billing_week=str(bw), customer=request.user.customer, status=AccountStatusChange.LEFT, ) account_status_change.save() return redirect(reverse('dashboard_bye')) # if a GET (or any other method) we'll create a blank form else: form = LeaveReasonForm() return render( request, 'dashboard/leave.html', { 'form': form, } )
def dashboard_skip_weeks(request): now = timezone.now() bw = get_billing_week(now) if request.method == 'POST' and request.POST.get('cancel'): messages.add_message( request, messages.INFO, 'Your skip weeks have not been changed.' ) return redirect(reverse("dashboard")) SkipFormSet = formset_factory( SkipForm, extra=0, formset=BaseSkipFormSet, ) # We want to see the next 9 skip weeks we can change # We can't change things in the current billing week but # but we can change things in the next billing week skipped_billing_weeks = [] if settings.ALLOW_SKIP_CURRENT_WEEK: startbw = bw bwqstr = str(bw) else: startbw = bw.next() bwqstr = str(startbw) for skip in Skip.objects.order_by( 'billing_week' ).filter( customer=request.user.customer, billing_week__gte=bwqstr, ): skipped_billing_weeks.append(skip.billing_week) valid_dates = {} initial_skips = [] # Offer dates this month and for the next two. for month, pickup_dates in get_pickup_dates(startbw.wed, 9).items(): for skipbw in pickup_dates: pickup_date = skipbw.wed skip_choice = { 'id': str(skipbw), 'skipbw': skipbw, 'skipped': str(skipbw) in skipped_billing_weeks, } initial_skips.append(skip_choice) valid_dates[str(skipbw)] = skip_choice if request.method == 'POST': formset = SkipFormSet( request.POST, request.FILES, initial=initial_skips, ) if formset.is_valid(): to_skip = [] to_unskip = [] for row in formset.cleaned_data: skipbw = row['id'] if row['skipped'] != valid_dates[skipbw]['skipped']: if row['skipped'] is True: to_skip.append(skipbw) else: to_unskip.append(skipbw) if not to_skip and not to_unskip: messages.add_message( request, messages.ERROR, ''' You haven't made any changes to your skip weeks. You can click Cancel if you are happy with your skip weeks as they are. ''' ) return redirect(reverse("dashboard_skip_weeks")) for skipbw in to_skip: assert skipbw not in to_unskip skip = Skip( created=now, created_in_billing_week=str(bw), billing_week=skipbw, customer=request.user.customer, ) skip.save() for skipbw in to_unskip: skip = Skip.objects.filter( customer=request.user.customer, billing_week=skipbw ).get() skip.delete() messages.add_message( request, messages.SUCCESS, 'Your skip weeks have been updated successfully' ) return redirect(reverse("dashboard")) else: formset = SkipFormSet(initial=initial_skips) return render( request, 'dashboard/skip-weeks.html', {'formset': formset} )
def gocardless_callback(request): if settings.GOCARDLESS_ENVIRONMENT == 'sandbox' and \ request.GET.get('skip', '').lower() == 'true': assert settings.SKIP_GOCARDLESS mandate = list(BillingGoCardlessMandate.objects.filter( customer=request.user.customer, ).all())[-1] mandate.gocardless_mandate_id = str(uuid.uuid4()) else: mandate = BillingGoCardlessMandate.objects.filter( customer=request.user.customer, gocardless_redirect_flow_id=request.GET['redirect_flow_id'], ).get() complete_redirect_flow = settings.GOCARDLESS_CLIENT.redirect_flows.complete( mandate.gocardless_redirect_flow_id, {'session_token': mandate.session_token} ) assert complete_redirect_flow.id == mandate.gocardless_redirect_flow_id mandate.gocardless_mandate_id = complete_redirect_flow.links.mandate now = timezone.now() bw = get_billing_week(now) mandate.in_use_for_customer = request.user.customer mandate.completed = now mandate.completed_in_billing_week = str(bw) mandate.save() tags = CustomerTag.objects.filter(tag='Starter').all() if tags: request.user.customer.tags.add(tags[0]) request.user.customer.save() # Now, take the first payment if it is needed # Get the amount and date from the session. number, amount_pounds, amount_per_week, first_bw_of_next_month = _get_cost_for_billing_week(request.user.customer, bw) # If this isn't true, it is because it took over a week to complete GoCardless and this has to be dealt with manually. assert amount_pounds <= mandate.amount_notified assert int(amount_pounds*100) == (amount_pounds*100) if amount_pounds: # We don't want to make a payment unless there is something to pay. mandate_id = request.user.customer.gocardless_current_mandate.gocardless_mandate_id if settings.SKIP_GOCARDLESS: payment_response_id = str(uuid.uuid4()) payment_response_status = 'skipped' else: payment_response_id, payment_response_status = Payment.send_to_gocardless(mandate_id, amount_pounds) payment = Payment( customer=request.user.customer, gocardless_mandate_id=mandate_id, amount=amount_pounds, reason=Payment.JOIN_WITH_COLLECTIONS_AVAILABLE, gocardless_payment_id=payment_response_id, created=now, created_in_billing_week=str(bw), ) payment.save() payment_status_change = PaymentStatusChange( changed=now, changed_in_billing_week=str(bw), status=payment_response_status, payment=payment, ) payment_status_change.save() bill_against = bw.next() for x in range(number): li = LineItem( payment=payment, created=now, created_in_billing_week=bw, bill_against=str(bill_against), customer=request.user.customer, amount=amount_per_week, reason=LineItem.NEW_JOINER ) li.save() bill_against = bw.next() account_status_change = AccountStatusChange( changed=now, changed_in_billing_week=str(bw), customer=request.user.customer, status=AccountStatusChange.ACTIVE, ) account_status_change.save() messages.add_message( request, messages.INFO, 'Successfully set up Go Cardless.' ) return redirect(reverse("dashboard"))
def dashboard(request): if request.user.customer.account_status != AccountStatusChange.LEFT: latest_cp_change = CustomerCollectionPointChange.objects.order_by( '-changed' ).filter(customer=request.user.customer)[:1] latest_customer_order_change = CustomerOrderChange.objects.order_by( '-changed' ).filter(customer=request.user.customer)[:1] collection_point = latest_cp_change[0].collection_point now = timezone.now() bw = get_billing_week(now) weekday = now.weekday() skipped_billing_weeks = [] skipped = len( Skip.objects.order_by('billing_week').filter( customer=request.user.customer, billing_week=str(bw), ).all() ) > 0 if weekday == 6: # Sunday if collection_point.collection_day == 'WED': collection_date = 'Wednesday' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday' else: collection_date = 'Wednesday and Thursday' if timezone.now().hour < bw.end.hour: deadline = '3pm today' changes_affect = "next week's collection" else: deadline = '3pm next Sunday' changes_affect = "the collection after next" elif weekday == 0: # Monday if collection_point.collection_day == 'WED': collection_date = 'Wednesday' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday' else: collection_date = 'Wednesday and Thursday' deadline = '3pm this Sunday' changes_affect = "next week's collection" elif weekday == 1: # Tuesday if collection_point.collection_day == 'WED': collection_date = 'tomorrow' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday' else: collection_date = 'tomorrow and Thursday' deadline = '3pm this Sunday' changes_affect = "next week's collection" elif weekday == 2: # Wednesday if collection_point.collection_day == 'WED': collection_date = 'today' elif collection_point.collection_day == 'THURS': collection_date = 'tomorrow' else: collection_date = 'today and tomorrow' deadline = '3pm this Sunday' changes_affect = "next week's collection" elif weekday == 3: # Thurs if collection_point.collection_day == 'WED': collection_date = 'Wednesday next week' elif collection_point.collection_day == 'THURS': collection_date = 'today' else: collection_date = 'today' deadline = '3pm this Sunday' changes_affect = "next week's collection" else: if collection_point.collection_day == 'WED': collection_date = 'Wednesday next week' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday next week' else: collection_date = 'Wednesday and Thursday next week' if weekday == 4: # Friday deadline = '3pm this Sunday' else: # Saturday deadline = '3pm tomorrow' changes_affect = "next week's collection" # fall back for the case when we have a user just starting this week if request.user.customer.created_in_billing_week == bw: if collection_point.collection_day == 'WED': collection_date = 'Wednesday next week' elif collection_point.collection_day == 'THURS': collection_date = 'Thursday next week' else: collection_date = 'Wednesday and Thursday next week' if weekday == 4: # Friday deadline = '3pm this Sunday' else: # Saturday deadline = '3pm tomorrow' changes_affect = "next week's collection" bag_quantities = CustomerOrderChangeBagQuantity.objects.filter( customer_order_change=latest_customer_order_change ).all() if skipped: collection_date = collection_date.replace(' and ', ' or ') if not ( collection_date.startswith('today') or collection_date.startswith('tomorrow') ): collection_date = 'on '+ collection_date return render( request, 'dashboard/index.html', { 'bag_quantities': bag_quantities, 'collection_point': collection_point, 'collection_date': collection_date, 'deadline': deadline, 'changes_affect': changes_affect, 'skipped': skipped, } ) else: return render( request, 'dashboard/re-join-scheme.html', )
def setUp(self): # Freeze time in UTC with freezegun.freeze_time('2015-11-01 00:00', tick=False): bag_type1 = BagType.objects.create(name='Large Bag') bag_type1.weekly_cost = Decimal('8.00') bag_type2 = BagType.objects.create(name='Small Bag') bag_type2.weekly_cost = Decimal('5.52') # In billing week 3, just before the start of billing week 4 (the last of the month) with freezegun.freeze_time('2015-11-22 11:50', tick=False): # Customer signs up with one collection left in July now3 = timezone.now() bw3 = get_billing_week(now3) print('Creating Customer One in {}'.format(bw3)) self.customer1 = Customer.objects.create_now( full_name='Customer One') self.customer1._set_bag_quantities({ bag_type1: 1, bag_type2: 2 }, reason='JOIN') payment = Payment( customer=self.customer1, gocardless_mandate_id='xxx', amount=Decimal('19.04'), reason=Payment.JOIN_WITH_COLLECTIONS_AVAILABLE, gocardless_payment_id='xxx', created=now3, created_in_billing_week=str(bw3), ) payment.save() payment_status_change = PaymentStatusChange( changed=now3, changed_in_billing_week=str(bw3), status='xxx', payment=payment, ) payment_status_change.save() for x in range(1): li = LineItem( payment=payment, created=now3, created_in_billing_week=bw3, # The changes are from now: bill_against=str(bw3.next()), customer=self.customer1, amount=Decimal('19.04'), reason=LineItem.NEW_JOINER) li.save() account_status_change = AccountStatusChange( changed=now3, changed_in_billing_week=str(bw3), customer=self.customer1, status=AccountStatusChange.ACTIVE, ) account_status_change.save() # Just after the deadline for billing week 4 => No billing dates left this month with freezegun.freeze_time('2015-11-22 15:10', tick=False): # Customer signs up with no collections left in July now4 = timezone.now() bw4 = get_billing_week(now4) print('Creating Customer Two in {}'.format(bw4)) assert bw4 == bw3.next() self.customer2 = Customer.objects.create_now( full_name='Customer Two') self.customer2._set_bag_quantities({ bag_type1: 1, bag_type2: 3 }, reason='JOIN') account_status_change = AccountStatusChange( changed=now4, changed_in_billing_week=str(bw4), customer=self.customer2, status=AccountStatusChange.ACTIVE, ) account_status_change.save() # Customer signs up but never completes GoCardless print('Creating Customer Three in {}'.format(bw4)) self.customer3 = Customer.objects.create_now( full_name='Customer Three') self.customer3._set_bag_quantities({ bag_type1: 1, bag_type2: 3 }, reason='JOIN') account_status_change = AccountStatusChange( changed=now4, changed_in_billing_week=str(bw4), customer=self.customer3, status=AccountStatusChange.AWAITING_DIRECT_DEBIT, ) account_status_change.save() # We change Customer 2's order (from 3 to 2 bag types) and set some skip weeks: with freezegun.freeze_time( '2015-12-09 15:10', tick=False ): # Change in billing week 2, takes effect billing week 3 now2 = timezone.now() bw2 = get_billing_week(now2) print('Changing Customer Two in {}'.format(bw2)) self.customer2._set_bag_quantities({ bag_type1: 1, bag_type2: 2 }, reason='JOIN') skip = Skip(customer=self.customer2, billing_week=bw2) skip.save() # Skip the same week as the order change above skip = Skip(customer=self.customer2, billing_week=parse_billing_week('2015-12 3')) skip.save()
def create_line_items(year, month, start_customer=0): ''' We run this immediately after the billing date affecting the first billing week of the month. So, if given 2016, 8 to run a bill in advance for August, the code should be run on Sunday 31 July 2016 just after 3pm GMT. We will create line items for August, and adjustments due during July. XXX What happens if someone leaves - should we automatically set skip weeks until the end of the month -> Then Leave status only applies on the month following and skip weeks is what causes the refund XXX Also need to make sure that collection points don't show up for LEAVEs ''' # e.g. 31st July 2016 now = timezone.now() last_run = LineItemRun.objects.order_by('-started')[:1] if last_run: assert last_run[0].started < now, 'The last run was in the future' line_item_run = LineItemRun.objects.create( job_id='xxx', year=year, month=month, started=now, finished=None, start_customer=start_customer or None, currently_processing_customer=None, ) line_item_run.save() line_items_by_customer = {} now_bw = get_billing_week(now) # Find the last first billing week of the previous month billing_week = future_billing_week = parse_billing_week( '{0}-{1:02d} {2}'.format(year, month, 1)) assert now_bw >= billing_week, 'Cannot run line items for future weeks' billing_weeks_next_month = [] while future_billing_week.month == month: billing_weeks_next_month.append(future_billing_week) future_billing_week = future_billing_week.next() old_billing_week = start = billing_week.prev() billing_weeks_last_month = [] while old_billing_week.month == start.month: billing_weeks_last_month.append(old_billing_week) old_billing_week = old_billing_week.prev() billing_weeks_last_month.reverse() print(billing_weeks_last_month) print(billing_weeks_next_month) # For each customer customers = Customer.objects.order_by('pk').filter( created__lt=billing_weeks_next_month[0].start) print(customers.all()) for customer in customers: if customer.pk < start_customer: continue if not line_item_run.start_customer: line_item_run.start_customer = customer line_item_run.currently_processing_customer = customer line_item_run.save() print( 'Processing', customer, customer.account_status_before(billing_weeks_next_month[0].start)) if customer.account_status not in ( AccountStatusChange.AWAITING_DIRECT_DEBIT): # For each billing week in the previous month... for past_billing_week in billing_weeks_last_month: # Look up what the order actually was bag_quantities = CustomerOrderChange.as_of(past_billing_week, customer=customer) should_have_billed = calculate_weekly_fee(bag_quantities) # Look up the line item for that week lis = LineItem.objects.filter( customer=customer, bill_against=str(past_billing_week), reason__in=(LineItem.REGULAR, LineItem.NEW_JOINER), ).all() assert len(lis) <= 1 if len(lis) == 1: billed = lis[0].amount else: billed = 0 print('Billed:', billed, 'Should have billed:', should_have_billed) correction = should_have_billed - billed if correction != 0: cli = LineItem( amount=correction, created=now, created_in_billing_week=now_bw, customer=customer, bill_against=str(past_billing_week), reason=LineItem.ORDER_CHANGE_ADJUSTMENT, ) cli.save() line_items_by_customer.setdefault(customer, []).append(cli) # If there is a skip week, refund the cost of the new order skips = Skip.objects.filter( customer=customer, billing_week=past_billing_week).all() assert len(skips) <= 1 if len(skips): print('Skips:', skips, past_billing_week, -should_have_billed) cli = LineItem( amount=-should_have_billed, created=now, created_in_billing_week=now_bw, customer=customer, bill_against=str(past_billing_week), reason=LineItem.SKIP_REFUND, ) cli.save() line_items_by_customer.setdefault(customer, []).append(cli) # If the customer has left, or not set up don't bill them: if customer.account_status_before( billing_weeks_next_month[0].start) not in ( AccountStatusChange.AWAITING_DIRECT_DEBIT, AccountStatusChange.LEFT): # For each billing week in the next month, add a line item to the order print('Adding in line items for next months order') next_month_bag_quantities = CustomerOrderChange.as_of( billing_weeks_next_month[0], customer=customer, ) weekly_cost = calculate_weekly_fee(next_month_bag_quantities) for future_billing_week in billing_weeks_next_month: li = LineItem( amount=weekly_cost, created=now, created_in_billing_week=now_bw, customer=customer, bill_against=str(future_billing_week), reason=LineItem.REGULAR, ) li.save() line_items_by_customer.setdefault(customer, []).append(li) return line_items_by_customer
def setUp(self): # Freeze time in UTC with freezegun.freeze_time('2015-11-01 00:00', tick=False): bag_type1 = BagType.objects.create(name='Large Bag') bag_type1.weekly_cost=Decimal('8.00') bag_type2 = BagType.objects.create(name='Small Bag') bag_type2.weekly_cost=Decimal('5.52') # In billing week 3, just before the start of billing week 4 (the last of the month) with freezegun.freeze_time('2015-11-22 11:50', tick=False): # Customer signs up with one collection left in July now3 = timezone.now() bw3 = get_billing_week(now3) print('Creating Customer One in {}'.format(bw3)) self.customer1 = Customer.objects.create_now(full_name='Customer One') self.customer1._set_bag_quantities({bag_type1: 1, bag_type2: 2}, reason='JOIN') payment = Payment( customer=self.customer1, gocardless_mandate_id='xxx', amount=Decimal('19.04'), reason=Payment.JOIN_WITH_COLLECTIONS_AVAILABLE, gocardless_payment_id='xxx', created=now3, created_in_billing_week=str(bw3), ) payment.save() payment_status_change = PaymentStatusChange( changed=now3, changed_in_billing_week=str(bw3), status='xxx', payment=payment, ) payment_status_change.save() for x in range(1): li = LineItem( payment=payment, created=now3, created_in_billing_week=bw3, # The changes are from now: bill_against=str(bw3.next()), customer=self.customer1, amount=Decimal('19.04'), reason=LineItem.NEW_JOINER ) li.save() account_status_change = AccountStatusChange( changed=now3, changed_in_billing_week=str(bw3), customer=self.customer1, status=AccountStatusChange.ACTIVE, ) account_status_change.save() # Just after the deadline for billing week 4 => No billing dates left this month with freezegun.freeze_time('2015-11-22 15:10', tick=False): # Customer signs up with no collections left in July now4 = timezone.now() bw4 = get_billing_week(now4) print('Creating Customer Two in {}'.format(bw4)) assert bw4 == bw3.next() self.customer2 = Customer.objects.create_now(full_name='Customer Two') self.customer2._set_bag_quantities({bag_type1: 1, bag_type2: 3}, reason='JOIN') account_status_change = AccountStatusChange( changed=now4, changed_in_billing_week=str(bw4), customer=self.customer2, status=AccountStatusChange.ACTIVE, ) account_status_change.save() # Customer signs up but never completes GoCardless print('Creating Customer Three in {}'.format(bw4)) self.customer3 = Customer.objects.create_now(full_name='Customer Three') self.customer3._set_bag_quantities({bag_type1: 1, bag_type2: 3}, reason='JOIN') account_status_change = AccountStatusChange( changed=now4, changed_in_billing_week=str(bw4), customer=self.customer3, status=AccountStatusChange.AWAITING_DIRECT_DEBIT, ) account_status_change.save() # We change Customer 2's order (from 3 to 2 bag types) and set some skip weeks: with freezegun.freeze_time('2015-12-09 15:10', tick=False): # Change in billing week 2, takes effect billing week 3 now2 = timezone.now() bw2 = get_billing_week(now2) print('Changing Customer Two in {}'.format(bw2)) self.customer2._set_bag_quantities({bag_type1: 1, bag_type2: 2}, reason='JOIN') skip = Skip(customer=self.customer2, billing_week=bw2) skip.save() # Skip the same week as the order change above skip = Skip(customer=self.customer2, billing_week=parse_billing_week('2015-12 3')) skip.save()
def create_line_items(year, month, start_customer=0): ''' We run this immediately after the billing date affecting the first billing week of the month. So, if given 2016, 8 to run a bill in advance for August, the code should be run on Sunday 31 July 2016 just after 3pm GMT. We will create line items for August, and adjustments due during July. XXX What happens if someone leaves - should we automatically set skip weeks until the end of the month -> Then Leave status only applies on the month following and skip weeks is what causes the refund XXX Also need to make sure that collection points don't show up for LEAVEs ''' # e.g. 31st July 2016 now = timezone.now() last_run = LineItemRun.objects.order_by('-started')[:1] if last_run: assert last_run[0].started < now, 'The last run was in the future' line_item_run = LineItemRun.objects.create( job_id = 'xxx', year = year, month = month, started = now, finished =None, start_customer = start_customer or None, currently_processing_customer = None, ) line_item_run.save() line_items_by_customer = {} now_bw = get_billing_week(now) # Find the last first billing week of the previous month billing_week = future_billing_week = parse_billing_week('{0}-{1:02d} {2}'.format(year, month, 1)) assert now_bw >= billing_week, 'Cannot run line items for future weeks' billing_weeks_next_month = [] while future_billing_week.month == month: billing_weeks_next_month.append(future_billing_week) future_billing_week = future_billing_week.next() old_billing_week = start = billing_week.prev() billing_weeks_last_month = [] while old_billing_week.month == start.month: billing_weeks_last_month.append(old_billing_week) old_billing_week = old_billing_week.prev() billing_weeks_last_month.reverse() print(billing_weeks_last_month) print(billing_weeks_next_month) # For each customer customers = Customer.objects.order_by('pk').filter(created__lt=billing_weeks_next_month[0].start) print(customers.all()) for customer in customers: if customer.pk < start_customer: continue if not line_item_run.start_customer: line_item_run.start_customer = customer line_item_run.currently_processing_customer = customer line_item_run.save() print('Processing', customer, customer.account_status_before(billing_weeks_next_month[0].start)) if customer.account_status not in (AccountStatusChange.AWAITING_DIRECT_DEBIT): # For each billing week in the previous month... for past_billing_week in billing_weeks_last_month: # Look up what the order actually was bag_quantities = CustomerOrderChange.as_of(past_billing_week, customer=customer) should_have_billed = calculate_weekly_fee(bag_quantities) # Look up the line item for that week lis = LineItem.objects.filter( customer=customer, bill_against=str(past_billing_week), reason__in=(LineItem.REGULAR, LineItem.NEW_JOINER), ).all() assert len(lis) <= 1 if len(lis) == 1: billed = lis[0].amount else: billed = 0 print('Billed:', billed, 'Should have billed:', should_have_billed) correction = should_have_billed - billed if correction != 0: cli = LineItem( amount=correction, created=now, created_in_billing_week=now_bw, customer=customer, bill_against=str(past_billing_week), reason=LineItem.ORDER_CHANGE_ADJUSTMENT, ) cli.save() line_items_by_customer.setdefault(customer, []).append(cli) # If there is a skip week, refund the cost of the new order skips = Skip.objects.filter(customer=customer, billing_week=past_billing_week).all() assert len(skips) <= 1 if len(skips): print('Skips:', skips, past_billing_week, -should_have_billed) cli = LineItem( amount=-should_have_billed, created=now, created_in_billing_week=now_bw, customer=customer, bill_against=str(past_billing_week), reason=LineItem.SKIP_REFUND, ) cli.save() line_items_by_customer.setdefault(customer, []).append(cli) # If the customer has left, or not set up don't bill them: if customer.account_status_before(billing_weeks_next_month[0].start) not in (AccountStatusChange.AWAITING_DIRECT_DEBIT, AccountStatusChange.LEFT): # For each billing week in the next month, add a line item to the order print('Adding in line items for next months order') next_month_bag_quantities = CustomerOrderChange.as_of( billing_weeks_next_month[0], customer=customer, ) weekly_cost = calculate_weekly_fee(next_month_bag_quantities) for future_billing_week in billing_weeks_next_month: li = LineItem( amount=weekly_cost, created=now, created_in_billing_week=now_bw, customer=customer, bill_against=str(future_billing_week), reason=LineItem.REGULAR, ) li.save() line_items_by_customer.setdefault(customer, []).append(li) return line_items_by_customer
def handle(self, *args, **options): json_path = options['path'] self.stdout.write( self.style.NOTICE("looking up this path: {}".format(json_path))) with open(json_path) as data_file: blob_o_stuff = json.load(data_file) customers = [ item for item in blob_o_stuff if item['model'] == 'customers.customer' ] bag_choices = [ item for item in blob_o_stuff if item['model'] == 'customers.bagchoice' ] gc_subs = [ item for item in blob_o_stuff if item['model'] == 'customers.gcsubscription' ] self.stdout.write( self.style.NOTICE("Found {} customer entries".format( len(customers)))) self.stdout.write( self.style.NOTICE("Found {} bagchoice entries".format( len(bag_choices)))) self.stdout.write( self.style.NOTICE("Found {} gc_sub entries".format( len(gc_subs)))) total_customers = len(customers) for (index, c) in enumerate(customers): user = self._make_user(c) now = timezone.now() bw = get_billing_week(now) cf = c['fields'] customer = Customer(created=now, created_in_billing_week=str(bw), full_name="{} {}".format( cf['first_name'], cf['surname']), nickname=cf['first_name'], mobile=cf['telephone_1'], user=user, id=c['pk']) customer.save() account_status_change = AccountStatusChange( changed=now, changed_in_billing_week=str(bw), customer=customer, status=AccountStatusChange.ACTIVE, ) account_status_change.save() old_bag_choices = [ b for b in bag_choices if b['fields']['customer'] == c['pk'] ] customer.bag_quantities = self._convert_bag_choices( old_bag_choices) customer.collection_point = cf['pickup'] self.stdout.write( self.style.SUCCESS("Imported {} of {}: {}".format( index, total_customers, customer.full_name)))