From 4bb24e8ff9ab4e826b7f103edf51d82f5e46ff6d Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Thu, 7 Jul 2016 14:56:53 -0400 Subject: [PATCH] Invoice tool for generating accounting compliant reports. --- tools/invoices.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tools/invoices.py diff --git a/tools/invoices.py b/tools/invoices.py new file mode 100644 index 000000000..f6c568573 --- /dev/null +++ b/tools/invoices.py @@ -0,0 +1,138 @@ +import stripe as _stripe +_stripe.api_version = '2016-06-15' + +import logging +import time +import sys +import csv +import codecs + +from itertools import groupby +from datetime import datetime, timedelta, date +from cStringIO import StringIO + +from app import billing as stripe + + +def _format_timestamp(stripe_timestamp): + date_obj = date.fromtimestamp(stripe_timestamp) + return date_obj.strftime('%m/%d/%Y') + + +def _format_money(stripe_money): + return stripe_money/100.0 + + +def list_charges(num_days): + """ List all charges that have occurred in the past specified number of days. + """ + now = datetime.utcnow() + starting_from = now - timedelta(days=num_days) + starting_timestamp = str(int(time.mktime(starting_from.timetuple()))) + created = {'gte': starting_timestamp} + starting_after_kw = {} + + while True: + charges = stripe.Charge.list(limit=100, expand=['data.invoice'], created=created, + **starting_after_kw) + for charge in charges.data: + yield charge + + if not charges.has_more: + break + + starting_after_kw = {'starting_after': charge.id} + + +def _date_key(line_item): + return line_item.start, line_item.end + + +def expand_invoice(invoice, total_amount): + if invoice is None: + yield total_amount, None, None, None + else: + data_iter = groupby(invoice.lines.data, lambda li: (li.period.start, li.period.end)) + for (period_start, period_end), line_items_iter in data_iter: + line_items = list(line_items_iter) + period_amount = sum(line_item.amount for line_item in line_items) + yield period_amount, period_start, period_end, line_items[-1].plan + + +def format_charge(charge): + """ Generator which will return one or more line items corresponding to the + line items for this charge. + """ + ch_status = 'Paid' + if charge.failure_code is not None: + ch_status = 'Failed' + elif charge.amount_refunded > 0: + ch_status = 'Refunded' + + card = charge.source + + invoice_iterable = expand_invoice(charge.invoice, charge.amount) + for line_amount, period_start, period_end, plan in invoice_iterable: + yield [ + _format_timestamp(charge.created), + _format_timestamp(period_start) if period_start is not None else None, + _format_timestamp(period_end) if period_end is not None else None, + _format_money(line_amount), + 'credit_card', + ch_status, + plan.name if plan is not None else None, + charge.id, + charge.customer, + card.address_city, + card.address_state, + card.address_country, + card.address_zip, + card.country, + ] + + +class _UnicodeWriter(object): + """ + A CSV writer which will write rows to CSV file "f", + which is encoded in the given encoding. + """ + def __init__(self, f, dialect=csv.excel, encoding='utf-8', **kwds): + # Redirect output to a queue + self.queue = StringIO() + self.writer = csv.writer(self.queue, dialect=dialect, **kwds) + self.stream = f + self.encoder = codecs.getincrementalencoder(encoding)() + + @staticmethod + def _encode_cell(cell): + if cell is None: + return cell + return unicode(cell).encode('utf-8') + + def writerow(self, row): + self.writer.writerow([self._encode_cell(s) for s in row]) + # Fetch UTF-8 output from the queue ... + data = self.queue.getvalue() + data = data.decode('utf-8') + # ... and reencode it into the target encoding + data = self.encoder.encode(data) + # write to the target stream + self.stream.write(data) + # empty queue + self.queue.truncate(0) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.WARN) + + days = 30 + if len(sys.argv) > 1: + days = int(sys.argv[1]) + + transaction_writer = _UnicodeWriter(sys.stdout) + rows = (line_item + for one_charge in list_charges(days) + for line_item in format_charge(one_charge)) + for row in rows: + transaction_writer.writerow(row) + sys.stdout.flush()