Compare commits
10 commits
f0da9e5bbe
...
579f6dde4b
Author | SHA1 | Date | |
---|---|---|---|
579f6dde4b | |||
5f1315b4e4 | |||
|
950c7fd644 | ||
|
8b0fd52602 | ||
|
ac2359101b | ||
|
c714b2f41f | ||
|
1e24bfe304 | ||
|
742fccca0c | ||
|
c1d1e17afb | ||
|
15ef02c153 |
10 changed files with 173 additions and 17 deletions
|
@ -5,6 +5,7 @@ import functools
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
|
import pprint
|
||||||
import re
|
import re
|
||||||
import tokenize
|
import tokenize
|
||||||
|
|
||||||
|
@ -192,8 +193,18 @@ class AccountSplitter:
|
||||||
"template {!r}".format(self.template_name)
|
"template {!r}".format(self.template_name)
|
||||||
) from error
|
) from error
|
||||||
if sum(amt for _, amt in amounts) != 0:
|
if sum(amt for _, amt in amounts) != 0:
|
||||||
self._balance_amounts(amounts, template_vars['amount'])
|
try:
|
||||||
self._balance_amounts(amounts, -template_vars['amount'])
|
self._balance_amounts(amounts, template_vars['amount'])
|
||||||
|
self._balance_amounts(amounts, -template_vars['amount'])
|
||||||
|
except errors.UserInputConfigurationError as error:
|
||||||
|
printer = pprint.PrettyPrinter()
|
||||||
|
logger.debug(
|
||||||
|
"%s\n%s\n%s",
|
||||||
|
error.args[0],
|
||||||
|
printer.pformat(amounts),
|
||||||
|
printer.pformat(dict(template_vars)),
|
||||||
|
)
|
||||||
|
raise
|
||||||
return amounts
|
return amounts
|
||||||
|
|
||||||
def _iter_splits(self, template_vars):
|
def _iter_splits(self, template_vars):
|
||||||
|
|
|
@ -53,7 +53,12 @@ class _BrightFundsMixin:
|
||||||
|
|
||||||
|
|
||||||
class DonorReportImporter(_BrightFundsMixin, _csv.CSVImporterBase):
|
class DonorReportImporter(_BrightFundsMixin, _csv.CSVImporterBase):
|
||||||
pass
|
BOOK_KWARGS = {'encoding_override': 'utf-8'}
|
||||||
|
DATE_FMT = '%m/%d/%Y'
|
||||||
|
DATE_FIELD = 'Created'
|
||||||
|
DONOR_FIELD = 'Donor Name'
|
||||||
|
EMAIL_FIELD = 'Donor Email'
|
||||||
|
TYPE_FIELD = 'Type'
|
||||||
|
|
||||||
|
|
||||||
class DonorReportXLSImporter(_BrightFundsMixin, _xls.XLSImporterBase):
|
class DonorReportXLSImporter(_BrightFundsMixin, _xls.XLSImporterBase):
|
||||||
|
|
|
@ -29,7 +29,8 @@ class SalesImporter(_csv.CSVImporterBase):
|
||||||
ENTRY_SEED = {'currency': 'USD'}
|
ENTRY_SEED = {'currency': 'USD'}
|
||||||
|
|
||||||
def _read_row(self, row):
|
def _read_row(self, row):
|
||||||
if row['Order Type'] != 'Eventbrite Completed':
|
# if row['Order Type'] != 'Eventbrite Completed':
|
||||||
|
if row['Order Type'] == 'Free Order':
|
||||||
return None
|
return None
|
||||||
retval = {
|
retval = {
|
||||||
self.DECIMAL_FIELDS[key]: strparse.currency_decimal(row[key])
|
self.DECIMAL_FIELDS[key]: strparse.currency_decimal(row[key])
|
||||||
|
|
|
@ -6,24 +6,27 @@ from .. import strparse
|
||||||
class SponsorsImporter(_csv.CSVImporterBase):
|
class SponsorsImporter(_csv.CSVImporterBase):
|
||||||
DATE_FMT = '%Y-%m-%d %H:%M:%S %z'
|
DATE_FMT = '%Y-%m-%d %H:%M:%S %z'
|
||||||
NEEDED_FIELDS = frozenset([
|
NEEDED_FIELDS = frozenset([
|
||||||
'processed amount',
|
'transferred amount',
|
||||||
'status',
|
'transaction timestamp',
|
||||||
'transaction date',
|
|
||||||
])
|
])
|
||||||
COPIED_FIELDS = {
|
COPIED_FIELDS = {
|
||||||
'sponsor handle': 'handle',
|
'sponsor handle': 'handle',
|
||||||
# 'sponsor profile name': 'name',
|
# 'sponsor profile name': 'name',
|
||||||
'sponsor email': 'email',
|
'sponsor email': 'email',
|
||||||
'transaction id': 'transaction_id',
|
'stripe transfer id': 'transaction_id',
|
||||||
}
|
}
|
||||||
ENTRY_SEED = {'currency': 'USD'}
|
ENTRY_SEED = {'currency': 'USD'}
|
||||||
|
PAYEE_MAP = {
|
||||||
|
'PRIVATE': 'Anonymous',
|
||||||
|
}
|
||||||
|
|
||||||
def _read_row(self, row):
|
def _read_row(self, row):
|
||||||
amount = strparse.currency_decimal(row['processed amount'])
|
amount = strparse.currency_decimal(row['transferred amount'])
|
||||||
if (not amount) or row['status'] != 'settled':
|
if (not amount):
|
||||||
return None
|
return None
|
||||||
|
payee = row.get('sponsor profile name') or row['sponsor handle']
|
||||||
return {
|
return {
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
'date': strparse.date(row['transaction date'], self.DATE_FMT),
|
'date': strparse.date(row['transaction timestamp'], self.DATE_FMT),
|
||||||
'payee': row.get('sponsor profile name') or row['sponsor handle'],
|
'payee': self.PAYEE_MAP.get(payee, payee),
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,38 @@ import re
|
||||||
from . import _csv
|
from . import _csv
|
||||||
from .. import strparse
|
from .. import strparse
|
||||||
|
|
||||||
|
class EarningsImporter(_csv.CSVImporterBase):
|
||||||
|
AMOUNT_FIELDS = {
|
||||||
|
'Creator Share': 'net_amount',
|
||||||
|
'Creator Platform Fee': 'platform_fee',
|
||||||
|
'Creator Payment Processing Fee': 'processing_fee',
|
||||||
|
'Creator Currency Conversion Fee': 'currency_fee',
|
||||||
|
'Creator Equivalent of Patron Tax': 'tax_amount',
|
||||||
|
}
|
||||||
|
DATE_KEY = 'Date'
|
||||||
|
TYPE_KEY = 'Event Type'
|
||||||
|
NEEDED_FIELDS = frozenset([*AMOUNT_FIELDS, DATE_KEY, TYPE_KEY])
|
||||||
|
COPIED_FIELDS = {
|
||||||
|
'Creator Currency': 'currency',
|
||||||
|
'Event ID': 'event_id',
|
||||||
|
'Patron Email': 'email',
|
||||||
|
'Patron Name': 'payee',
|
||||||
|
'Patron User ID': 'user_id',
|
||||||
|
}
|
||||||
|
DATE_FMT = '%Y-%m-%d %H:%M:%S'
|
||||||
|
|
||||||
|
def _read_row(self, row):
|
||||||
|
if row[self.TYPE_KEY] != 'Payment':
|
||||||
|
return None
|
||||||
|
retval = {
|
||||||
|
ret_key: strparse.currency_decimal(row[src_key] or 0)
|
||||||
|
for src_key, ret_key in self.AMOUNT_FIELDS.items()
|
||||||
|
}
|
||||||
|
retval['amount'] = sum(retval.values()) - retval['tax_amount']
|
||||||
|
retval['date'] = strparse.date(row[self.DATE_KEY], self.DATE_FMT)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
class IncomeImporter(_csv.CSVImporterBase):
|
class IncomeImporter(_csv.CSVImporterBase):
|
||||||
AMOUNT_KEY = 'Pledge Amount'
|
AMOUNT_KEY = 'Pledge Amount'
|
||||||
DATE_KEY = 'Last Charge Date'
|
DATE_KEY = 'Last Charge Date'
|
||||||
|
@ -118,6 +150,7 @@ class Income2017Importer(_csv.CSVImporterBase):
|
||||||
'Status',
|
'Status',
|
||||||
])
|
])
|
||||||
COPIED_FIELDS = {
|
COPIED_FIELDS = {
|
||||||
|
'Email': 'email',
|
||||||
'Pledge': 'amount',
|
'Pledge': 'amount',
|
||||||
}
|
}
|
||||||
ENTRY_SEED = {
|
ENTRY_SEED = {
|
||||||
|
|
58
import2ledger/importers/printful.py
Normal file
58
import2ledger/importers/printful.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from . import _csv
|
||||||
|
from .. import strparse
|
||||||
|
|
||||||
|
|
||||||
|
class SalesImporter(_csv.CSVImporterBase):
|
||||||
|
DECIMAL_FIELDS = {
|
||||||
|
'Products': 'products',
|
||||||
|
'Shipping': 'shipping_fees',
|
||||||
|
'Tax': 'tax',
|
||||||
|
'VAT': 'vat',
|
||||||
|
'Total': 'amount',
|
||||||
|
'Discount': 'discount',
|
||||||
|
}
|
||||||
|
NEEDED_FIELDS = {'Printful ID', *DECIMAL_FIELDS}
|
||||||
|
COPIED_FIELDS = {}
|
||||||
|
CURRENCY_MAP = {
|
||||||
|
'$': 'USD',
|
||||||
|
'€': 'EUR',
|
||||||
|
'£': 'GBP',
|
||||||
|
'AU$': 'AUD',
|
||||||
|
'C$': 'CAD',
|
||||||
|
}
|
||||||
|
currency_regexp = re.compile(r'-?(?P<currency>\D*)\d.*')
|
||||||
|
|
||||||
|
def __init__(self, input_file):
|
||||||
|
# Orders.csv file is in UTF-8-SIG encoding and begins with a byte order
|
||||||
|
# mark character \ufeff. This happens *after* NEEDED_FIELDS is
|
||||||
|
# checked. This encoding check is done conditionally to handle the
|
||||||
|
# situation where someone has modified the CSV and saved as UTF-8.
|
||||||
|
first_char = input_file.read(1)
|
||||||
|
input_file.seek(0)
|
||||||
|
if first_char == '\ufeff':
|
||||||
|
input_file.reconfigure(encoding='UTF-8-SIG')
|
||||||
|
super().__init__(input_file)
|
||||||
|
|
||||||
|
def _read_row(self, row):
|
||||||
|
if row['Date'].startswith('Total paid'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Parse the currency values. A hyphen represents zero.
|
||||||
|
record = {
|
||||||
|
self.DECIMAL_FIELDS[key]: strparse.currency_decimal(row[key] if row[key] != '-' else '0')
|
||||||
|
for key in self.DECIMAL_FIELDS
|
||||||
|
}
|
||||||
|
|
||||||
|
# Most importers don't deal with multiple currencies. There are
|
||||||
|
# libraries that partly help with currency codes and symbols, but the
|
||||||
|
# codes/symbols Printful use have some quirks, so it's easier to do this
|
||||||
|
# ourselves.
|
||||||
|
currency_code = re.match(self.currency_regexp, row['Total']).group('currency')
|
||||||
|
record['currency'] = self.CURRENCY_MAP.get(currency_code, currency_code).replace('$', '')
|
||||||
|
|
||||||
|
record['date'] = strparse.date(row['Date'], '%B %d, %Y')
|
||||||
|
record['order_id'] = row['Order'].lstrip('Order ')
|
||||||
|
record['payee'] = 'Printful' # Not used but required to be set
|
||||||
|
return record
|
|
@ -1,15 +1,14 @@
|
||||||
import decimal
|
|
||||||
|
|
||||||
from . import _csv
|
from . import _csv
|
||||||
from .. import strparse
|
from .. import strparse
|
||||||
|
|
||||||
|
|
||||||
class PaymentImporter(_csv.CSVImporterBase):
|
class PaymentImporter(_csv.CSVImporterBase):
|
||||||
NEEDED_FIELDS = frozenset([
|
NEEDED_FIELDS = frozenset([
|
||||||
'Converted Currency',
|
'Converted Currency',
|
||||||
'Created (UTC)',
|
'Created (UTC)',
|
||||||
'Fee',
|
'Fee',
|
||||||
'Status',
|
'Status',
|
||||||
'Tax',
|
'Taxes On Fee',
|
||||||
])
|
])
|
||||||
COPIED_FIELDS = {
|
COPIED_FIELDS = {
|
||||||
'Card Name': 'payee',
|
'Card Name': 'payee',
|
||||||
|
@ -31,7 +30,7 @@ class PaymentImporter(_csv.CSVImporterBase):
|
||||||
'currency': row['Converted Currency'].upper(),
|
'currency': row['Converted Currency'].upper(),
|
||||||
'date': strparse.date(date_s, self.DATE_FMT),
|
'date': strparse.date(date_s, self.DATE_FMT),
|
||||||
'fee': strparse.currency_decimal(row['Fee']),
|
'fee': strparse.currency_decimal(row['Fee']),
|
||||||
'tax': strparse.currency_decimal(row['Tax']),
|
'tax': strparse.currency_decimal(row['Taxes On Fee']),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -30,7 +30,7 @@ REQUIREMENTS['tests_require'] = [
|
||||||
setup(
|
setup(
|
||||||
name='import2ledger',
|
name='import2ledger',
|
||||||
description="Import different sources of financial data to Ledger",
|
description="Import different sources of financial data to Ledger",
|
||||||
version='1.3.1',
|
version='1.4.5',
|
||||||
author='Brett Smith',
|
author='Brett Smith',
|
||||||
author_email='brettcsmith@brettcsmith.org',
|
author_email='brettcsmith@brettcsmith.org',
|
||||||
license='GNU AGPLv3+',
|
license='GNU AGPLv3+',
|
||||||
|
|
4
tests/data/PatreonEarnings2020.csv
Normal file
4
tests/data/PatreonEarnings2020.csv
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Date,Event ID,Event Type,Creator Currency,Creator Share,Creator Platform Fee,Creator Payment Processing Fee,Creator Currency Conversion Fee,Creator Equivalent of Patron Tax,Currency Conversion Rate,Patron Currency,Patron Charge Amount,Patron Tax Amount,Patron User ID,Patron Name,Patron Email
|
||||||
|
2020-12-03 04:05:06,30123456,Payment,USD,4.31,0.25,0.44,,1.00,,USD,6.00,1.00,32101234,Frances Jones,fjones@example.com
|
||||||
|
2020-12-02 03:04:05,34567890,Payment,USD,5.14,0.31,0.42,0.19,1.27,1212240,EUR,6.05,1.05,2345678,Barbara Johnson,bjohnson@example.com
|
||||||
|
2020-12-01 02:03:04,456789090,Payment,USD,12.64,0.70,0.66,,,,USD,14.00,,7654321,Alex Jones,ajones@example.com
|
|
|
@ -5,10 +5,12 @@
|
||||||
date: !!python/object/apply:datetime.date [2017, 9, 1]
|
date: !!python/object/apply:datetime.date [2017, 9, 1]
|
||||||
amount: !!python/object/apply:decimal.Decimal ["1500.00"]
|
amount: !!python/object/apply:decimal.Decimal ["1500.00"]
|
||||||
currency: USD
|
currency: USD
|
||||||
|
email: alex@example.org
|
||||||
- payee: Dakota Doe
|
- payee: Dakota Doe
|
||||||
date: !!python/object/apply:datetime.date [2017, 9, 1]
|
date: !!python/object/apply:datetime.date [2017, 9, 1]
|
||||||
amount: !!python/object/apply:decimal.Decimal ["12.00"]
|
amount: !!python/object/apply:decimal.Decimal ["12.00"]
|
||||||
currency: USD
|
currency: USD
|
||||||
|
email: ddoe@example.org
|
||||||
|
|
||||||
- source: PatreonPatronReport_2020-08-01.csv
|
- source: PatreonPatronReport_2020-08-01.csv
|
||||||
importer: patreon.Income2020AugustImporter
|
importer: patreon.Income2020AugustImporter
|
||||||
|
@ -76,6 +78,46 @@
|
||||||
amount: !!python/object/apply:decimal.Decimal ["117.03"]
|
amount: !!python/object/apply:decimal.Decimal ["117.03"]
|
||||||
currency: USD
|
currency: USD
|
||||||
|
|
||||||
|
- source: PatreonEarnings2020.csv
|
||||||
|
importer: patreon.EarningsImporter
|
||||||
|
expect:
|
||||||
|
- payee: Frances Jones
|
||||||
|
email: fjones@example.com
|
||||||
|
user_id: "32101234"
|
||||||
|
event_id: "30123456"
|
||||||
|
date: !!python/object/apply:datetime.date [2020, 12, 3]
|
||||||
|
currency: USD
|
||||||
|
amount: !!python/object/apply:decimal.Decimal ["5"]
|
||||||
|
tax_amount: !!python/object/apply:decimal.Decimal ["1"]
|
||||||
|
net_amount: !!python/object/apply:decimal.Decimal ["4.31"]
|
||||||
|
platform_fee: !!python/object/apply:decimal.Decimal ["0.25"]
|
||||||
|
processing_fee: !!python/object/apply:decimal.Decimal ["0.44"]
|
||||||
|
currency_fee: !!python/object/apply:decimal.Decimal ["0"]
|
||||||
|
- payee: Barbara Johnson
|
||||||
|
email: bjohnson@example.com
|
||||||
|
user_id: "2345678"
|
||||||
|
event_id: "34567890"
|
||||||
|
date: !!python/object/apply:datetime.date [2020, 12, 2]
|
||||||
|
currency: USD
|
||||||
|
amount: !!python/object/apply:decimal.Decimal ["6.06"]
|
||||||
|
tax_amount: !!python/object/apply:decimal.Decimal ["1.27"]
|
||||||
|
net_amount: !!python/object/apply:decimal.Decimal ["5.14"]
|
||||||
|
platform_fee: !!python/object/apply:decimal.Decimal ["0.31"]
|
||||||
|
processing_fee: !!python/object/apply:decimal.Decimal ["0.42"]
|
||||||
|
currency_fee: !!python/object/apply:decimal.Decimal ["0.19"]
|
||||||
|
- payee: Alex Jones
|
||||||
|
email: ajones@example.com
|
||||||
|
user_id: "7654321"
|
||||||
|
event_id: "456789090"
|
||||||
|
date: !!python/object/apply:datetime.date [2020, 12, 1]
|
||||||
|
currency: USD
|
||||||
|
amount: !!python/object/apply:decimal.Decimal ["14"]
|
||||||
|
tax_amount: !!python/object/apply:decimal.Decimal ["0"]
|
||||||
|
net_amount: !!python/object/apply:decimal.Decimal ["12.64"]
|
||||||
|
platform_fee: !!python/object/apply:decimal.Decimal [".7"]
|
||||||
|
processing_fee: !!python/object/apply:decimal.Decimal [".66"]
|
||||||
|
currency_fee: !!python/object/apply:decimal.Decimal ["0"]
|
||||||
|
|
||||||
- source: PatreonEarnings.csv
|
- source: PatreonEarnings.csv
|
||||||
importer: patreon.CardFeesImporter
|
importer: patreon.CardFeesImporter
|
||||||
expect:
|
expect:
|
||||||
|
|
Loading…
Reference in a new issue