hooks.ledger_entry: Look up templates dynamically.

If there's a 'ledger entry' key in the entry data, use that value as the
name of the template to load.  Thanks to this, nbpy2017 could collapse
multiple importers into one.

Otherwise, build a default template name based on the importer source, and
try to use that.

All the configuration names now end with "ledger entry" instead of starting
with "template".  This makes it clearer what they're for, in case we
support other kinds of output templates in the future.

I ended up changing the names of some of the importers so the default
template name was nice, rather than specifying template names for all of
them, to reduce the amount of name discrepancies across the codebase.
This commit is contained in:
Brett Smith 2017-12-31 18:52:30 -05:00
parent 475de6db4e
commit db59d2fc8c
13 changed files with 68 additions and 105 deletions

View file

@ -39,9 +39,6 @@ Class method ``can_handle(source_file)``
``__iter__()`` ``__iter__()``
Returns a iterator of entry data dicts. Returns a iterator of entry data dicts.
Class attribute ``TEMPLATE_KEY``
A string with the full key to load the corresponding template from the user's configuration (e.g., ``'template patreon income'``).
Hooks Hooks
~~~~~ ~~~~~
@ -62,17 +59,6 @@ Hooks make arbitrary transformations to entry data dicts. Every entry data dict
Class attribute ``KIND`` Class attribute ``KIND``
This should be one of the values of the ``hooks.HOOK_KINDS`` enum. This information determines what order hooks run in. This should be one of the values of the ``hooks.HOOK_KINDS`` enum. This information determines what order hooks run in.
Templates
~~~~~~~~~
Templates receive entry data dicts and format them into final output entries.
``__init__(template_str)``
Initializes the template from a single string, as read from the user's configuration.
``render(entry_data)``
Returns a string with the output entry, using the given entry data.
Loading importers and hooks Loading importers and hooks
--------------------------- ---------------------------

View file

@ -29,7 +29,7 @@ A template looks like a Ledger entry with a couple of differences:
Here's a simple template for Patreon patron payments:: Here's a simple template for Patreon patron payments::
[DEFAULT] [DEFAULT]
template patreon income = patreon income ledger entry =
;Tag: Value ;Tag: Value
Income:Patreon -{amount} Income:Patreon -{amount}
Accrued:Accounts Receivable:Patreon {amount} Accrued:Accounts Receivable:Patreon {amount}
@ -38,7 +38,7 @@ Let's walk through this line by line.
Every setting in your configuration file has to be in a section. ``[DEFAULT]`` is the default section, and import2ledger reads configuration settings from here if you don't specify another one. This documentation explains how to use sections later. Every setting in your configuration file has to be in a section. ``[DEFAULT]`` is the default section, and import2ledger reads configuration settings from here if you don't specify another one. This documentation explains how to use sections later.
``template patreon income =`` specifies which entry template this is. Every template is found from a setting with a name in the pattern ``template <SOURCE> <TYPE>``. The remaining lines are indented further than this name; this defines a multiline value. Don't worry about the exact indentation of your template; import2ledger will indent its output nicely. ``patreon income ledger entry =`` specifies which entry template this is. Every template is found from a setting with a name in the pattern ``<SOURCE> <TYPE> ledger entry``. The remaining lines are indented further than this name; this defines a multiline value. Don't worry about the exact indentation of your template; import2ledger will indent its output nicely.
The first line of the template is a Ledger tag. The program will leave all kinds of tags and Ledger comments alone, except to indent them nicely. The first line of the template is a Ledger tag. The program will leave all kinds of tags and Ledger comments alone, except to indent them nicely.
@ -104,13 +104,13 @@ You can define the following templates.
Amazon Affiliate Program Amazon Affiliate Program
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
``template amazon earnings`` ``amazon earnings ledger entry``
Imports one transaction per month, summarizing all earnings over the month. Generated from Amazon's "Fee Earnings" report CSV. Imports one transaction per month, summarizing all earnings over the month. Generated from Amazon's "Fee Earnings" report CSV.
Benevity Benevity
^^^^^^^^ ^^^^^^^^
``template benevity payments`` ``benevity donation ledger entry``
Imports one transaction per row in Benevity's donations report CSV. Imports one transaction per row in Benevity's donations report CSV.
This template can use these variables: This template can use these variables:
@ -140,16 +140,16 @@ Benevity
Patreon Patreon
^^^^^^^ ^^^^^^^
``template patreon income`` ``patreon income ledger entry``
Imports one transaction per patron per month. Generated from Patreon's monthly patron report CSVs. Imports one transaction per patron per month. Generated from Patreon's monthly patron report CSVs.
``template patreon cardfees`` ``patreon cardfees ledger entry``
Imports one expense transaction per month for that month's credit card fees. Generated from Patreon's earnings report CSV. Imports one expense transaction per month for that month's credit card fees. Generated from Patreon's earnings report CSV.
``template patreon svcfees`` ``patreon servicefees ledger entry``
Imports one expense transaction per month for that month's Patreon service fees. Generated from Patreon's earnings report CSV. Imports one expense transaction per month for that month's Patreon service fees. Generated from Patreon's earnings report CSV.
``template patreon vat`` ``patreon vat ledger entry``
Imports one transaction per country per month each time Patreon withheld VAT. Generated from Patreon's VAT report CSV. Imports one transaction per country per month each time Patreon withheld VAT. Generated from Patreon's VAT report CSV.
This template can use these variables: This template can use these variables:
@ -166,7 +166,7 @@ Patreon
Stripe Stripe
^^^^^^ ^^^^^^
``template stripe payments`` ``stripe payment ledger entry``
Imports one transaction per payment. Generated from Stripe's payments CSV export. Imports one transaction per payment. Generated from Stripe's payments CSV export.
This template can use these variables: This template can use these variables:

View file

@ -4,19 +4,10 @@ TODO
Template multiplexing with action hooks Template multiplexing with action hooks
--------------------------------------- ---------------------------------------
The big idea: make it easier for hooks to customize *what* template(s) are rendered by moving more of the process into hooks—including template rendering itself.
Required:
* Make the main loop seed the entry data with information about the importer used.
* Move template rendering into a hook, where the template to load is determined by a value in the entry data.
Extra customizations after that's done:
* Add a hook that simply reads information from a configuration file section ``[template variables]`` and adds it to the entry data. * Add a hook that simply reads information from a configuration file section ``[template variables]`` and adds it to the entry data.
* Add a hook that changes what template to use based on other entry data. (This needs more specification.) * Add a hook that changes what template to use based on other entry data. (This needs more specification.)
OR, should this be a hook that just adds more entry data, and trusts the Ledger entry template to use it?
New importers New importers
------------- -------------

View file

@ -34,8 +34,9 @@ class FileImporter:
'source_stem': in_path.stem, 'source_stem': in_path.stem,
} }
for importer in importers: for importer in importers:
source_vars['importer_class'] = importer.__name__
source_vars['importer_module'] = importer.__module__
in_file.seek(0) in_file.seek(0)
source_vars['template'] = importer.TEMPLATE_KEY
for entry_data in importer(in_file): for entry_data in importer(in_file):
entry_data = collections.ChainMap(entry_data, source_vars) entry_data = collections.ChainMap(entry_data, source_vars)
for hook in self.hooks: for hook in self.hooks:

View file

@ -283,7 +283,7 @@ class LedgerEntryHook:
template_s = section_config[config_key] template_s = section_config[config_key]
except KeyError: except KeyError:
raise errors.UserInputConfigurationError( raise errors.UserInputConfigurationError(
"template not defined in [{}]".format(section_name), "Ledger template not defined in [{}]".format(section_name),
config_key, config_key,
) )
return Template( return Template(
@ -297,9 +297,16 @@ class LedgerEntryHook:
def run(self, entry_data): def run(self, entry_data):
try: try:
template = self._load_template(self.config, None, entry_data['template']) template_key = entry_data['ledger template']
except KeyError:
template_key = '{} {} ledger entry'.format(
strparse.rslice_words(entry_data['importer_module'], -1, '.', 1),
entry_data['importer_class'][:-8].lower(),
)
try:
template = self._load_template(self.config, None, template_key)
except errors.UserInputConfigurationError as error: except errors.UserInputConfigurationError as error:
if error.strerror.startswith('template not defined '): if error.strerror.startswith('Ledger template not defined '):
have_template = False have_template = False
else: else:
raise raise

View file

@ -43,7 +43,6 @@ class EarningsImporter(_csv.CSVImporterBase):
'Date Shipped': '1979-07-09', 'Date Shipped': '1979-07-09',
} }
NEEDED_FIELDS = frozenset(SENTINEL_ROW.keys()) NEEDED_FIELDS = frozenset(SENTINEL_ROW.keys())
TEMPLATE_KEY = 'template amazon earnings'
ENTRY_SEED = { ENTRY_SEED = {
'currency': 'USD', 'currency': 'USD',
'payee': 'Amazon', 'payee': 'Amazon',

View file

@ -1,7 +1,7 @@
from . import _csv from . import _csv
from .. import strparse from .. import strparse
class PaymentImporter(_csv.CSVImporterBase): class DonationsImporter(_csv.CSVImporterBase):
HEADER_FIELDS = { HEADER_FIELDS = {
'Currency': 'currency', 'Currency': 'currency',
'Disbursement ID': 'disbursement_id', 'Disbursement ID': 'disbursement_id',
@ -26,7 +26,6 @@ class PaymentImporter(_csv.CSVImporterBase):
'Transaction ID': 'transaction_id', 'Transaction ID': 'transaction_id',
'Donation Frequency': 'frequency', 'Donation Frequency': 'frequency',
} }
TEMPLATE_KEY = 'template benevity payments'
DATE_FMT = '%Y-%m-%d' DATE_FMT = '%Y-%m-%d'
NOT_SHARED = 'Not shared by donor' NOT_SHARED = 'Not shared by donor'

View file

@ -121,7 +121,10 @@ def _parse_invoice(parser_class, source_file):
except AttributeError: except AttributeError:
return None return None
class ImporterBase: class InvoiceImporter:
INVOICE_CLASS = Invoice2017
LEDGER_TEMPLATE_KEY_FMT = 'nbpy2017 {0} ledger entry'
@classmethod @classmethod
def _parse_invoice(cls, source_file): def _parse_invoice(cls, source_file):
return _parse_invoice(cls.INVOICE_CLASS, source_file) return _parse_invoice(cls.INVOICE_CLASS, source_file)
@ -135,17 +138,5 @@ class ImporterBase:
def __iter__(self): def __iter__(self):
for entry in self.invoice: for entry in self.invoice:
if entry['status'] == self.YIELD_STATUS: entry['ledger entry'] = self.LEDGER_TEMPLATE_KEY_FMT.format(entry['status'].lower())
yield entry yield entry
class Invoice2017Importer(ImporterBase):
TEMPLATE_KEY = 'template nbpy2017 invoice'
INVOICE_CLASS = Invoice2017
YIELD_STATUS = STATUS_INVOICED
class Payment2017Importer(ImporterBase):
TEMPLATE_KEY = 'template nbpy2017 payment'
INVOICE_CLASS = Invoice2017
YIELD_STATUS = STATUS_PAID

View file

@ -17,7 +17,6 @@ class IncomeImporter(_csv.CSVImporterBase):
ENTRY_SEED = { ENTRY_SEED = {
'currency': 'USD', 'currency': 'USD',
} }
TEMPLATE_KEY = 'template patreon income'
def __init__(self, input_file): def __init__(self, input_file):
super().__init__(input_file) super().__init__(input_file)
@ -48,16 +47,14 @@ class FeeImporterBase(_csv.CSVImporterBase):
} }
class PatreonFeeImporter(FeeImporterBase): class ServiceFeesImporter(FeeImporterBase):
AMOUNT_FIELD = 'Patreon Fee' AMOUNT_FIELD = 'Patreon Fee'
NEEDED_FIELDS = frozenset(['Month', AMOUNT_FIELD]) NEEDED_FIELDS = frozenset(['Month', AMOUNT_FIELD])
TEMPLATE_KEY = 'template patreon svcfees'
class CardFeeImporter(FeeImporterBase): class CardFeesImporter(FeeImporterBase):
AMOUNT_FIELD = 'Processing Fees' AMOUNT_FIELD = 'Processing Fees'
NEEDED_FIELDS = frozenset(['Month', AMOUNT_FIELD]) NEEDED_FIELDS = frozenset(['Month', AMOUNT_FIELD])
TEMPLATE_KEY = 'template patreon cardfees'
class VATImporter(FeeImporterBase): class VATImporter(FeeImporterBase):
@ -67,4 +64,3 @@ class VATImporter(FeeImporterBase):
'Country Code': 'country_code', 'Country Code': 'country_code',
'Country Name': 'country_name', 'Country Name': 'country_name',
} }
TEMPLATE_KEY = 'template patreon vat'

View file

@ -17,7 +17,6 @@ class PaymentImporter(_csv.CSVImporterBase):
'Description': 'description', 'Description': 'description',
'id': 'payment_id', 'id': 'payment_id',
} }
TEMPLATE_KEY = 'template stripe payments'
DATE_FMT = '%Y-%m-%d' DATE_FMT = '%Y-%m-%d'
def _read_row(self, row): def _read_row(self, row):

View file

@ -11,7 +11,7 @@
currency: USD currency: USD
- source: PatreonEarnings.csv - source: PatreonEarnings.csv
importer: patreon.PatreonFeeImporter importer: patreon.ServiceFeesImporter
expect: expect:
- payee: Patreon - payee: Patreon
date: !!python/object/apply:datetime.date [2017, 9, 1] date: !!python/object/apply:datetime.date [2017, 9, 1]
@ -23,7 +23,7 @@
currency: USD currency: USD
- source: PatreonEarnings.csv - source: PatreonEarnings.csv
importer: patreon.CardFeeImporter importer: patreon.CardFeesImporter
expect: expect:
- payee: Patreon - payee: Patreon
date: !!python/object/apply:datetime.date [2017, 9, 1] date: !!python/object/apply:datetime.date [2017, 9, 1]
@ -83,9 +83,10 @@
description: "Payment for invoice #100" description: "Payment for invoice #100"
- source: nbpy2017a.html - source: nbpy2017a.html
importer: nbpy2017.Invoice2017Importer importer: nbpy2017.InvoiceImporter
expect: expect:
- payee: Python Person A - payee: Python Person A
ledger entry: nbpy2017 invoice ledger entry
date: !!python/object/apply:datetime.date [2017, 10, 19] date: !!python/object/apply:datetime.date [2017, 10, 19]
amount: !!python/object/apply:decimal.Decimal ["80.00"] amount: !!python/object/apply:decimal.Decimal ["80.00"]
tickets_sold: !!python/object/apply:decimal.Decimal ["1"] tickets_sold: !!python/object/apply:decimal.Decimal ["1"]
@ -96,41 +97,8 @@
status: Invoice status: Invoice
invoice_id: "83" invoice_id: "83"
invoice_date: !!python/object/apply:datetime.date [2017, 10, 19] invoice_date: !!python/object/apply:datetime.date [2017, 10, 19]
- source: nbpy2017b.html
importer: nbpy2017.Invoice2017Importer
expect:
- payee: Python Person B
date: !!python/object/apply:datetime.date [2017, 12, 3]
amount: !!python/object/apply:decimal.Decimal ["50.00"]
tickets_sold: !!python/object/apply:decimal.Decimal ["1"]
ticket_rate: !!python/object/apply:decimal.Decimal ["42.50"]
shirts_sold: !!python/object/apply:decimal.Decimal ["0"]
shirt_rate: !!python/object/apply:decimal.Decimal ["25.50"]
status: Invoice
currency: USD
invoice_date: !!python/object/apply:datetime.date [2017, 12, 3]
invoice_id: "304"
- source: nbpy2017c.html
importer: nbpy2017.Invoice2017Importer
expect:
- payee: Python Person C
date: !!python/object/apply:datetime.date [2017, 10, 5]
amount: !!python/object/apply:decimal.Decimal ["55.00"]
tickets_sold: !!python/object/apply:decimal.Decimal ["1"]
ticket_rate: !!python/object/apply:decimal.Decimal ["21.25"]
shirts_sold: !!python/object/apply:decimal.Decimal ["1"]
shirt_rate: !!python/object/apply:decimal.Decimal ["25.50"]
status: Invoice
currency: USD
invoice_date: !!python/object/apply:datetime.date [2017, 10, 5]
invoice_id: "11"
- source: nbpy2017a.html
importer: nbpy2017.Payment2017Importer
expect:
- payee: Python Person A - payee: Python Person A
ledger entry: nbpy2017 payment ledger entry
date: !!python/object/apply:datetime.date [2017, 10, 19] date: !!python/object/apply:datetime.date [2017, 10, 19]
amount: !!python/object/apply:decimal.Decimal ["80.00"] amount: !!python/object/apply:decimal.Decimal ["80.00"]
tickets_sold: !!python/object/apply:decimal.Decimal ["1"] tickets_sold: !!python/object/apply:decimal.Decimal ["1"]
@ -145,9 +113,22 @@
stripe_id: ch_ahr0ue8lai1ohqu4Gei4Biem stripe_id: ch_ahr0ue8lai1ohqu4Gei4Biem
- source: nbpy2017b.html - source: nbpy2017b.html
importer: nbpy2017.Payment2017Importer importer: nbpy2017.InvoiceImporter
expect: expect:
- payee: Python Person B - payee: Python Person B
ledger entry: nbpy2017 invoice ledger entry
date: !!python/object/apply:datetime.date [2017, 12, 3]
amount: !!python/object/apply:decimal.Decimal ["50.00"]
tickets_sold: !!python/object/apply:decimal.Decimal ["1"]
ticket_rate: !!python/object/apply:decimal.Decimal ["42.50"]
shirts_sold: !!python/object/apply:decimal.Decimal ["0"]
shirt_rate: !!python/object/apply:decimal.Decimal ["25.50"]
status: Invoice
currency: USD
invoice_date: !!python/object/apply:datetime.date [2017, 12, 3]
invoice_id: "304"
- payee: Python Person B
ledger entry: nbpy2017 payment ledger entry
date: !!python/object/apply:datetime.date [2017, 12, 3] date: !!python/object/apply:datetime.date [2017, 12, 3]
amount: !!python/object/apply:decimal.Decimal ["50.00"] amount: !!python/object/apply:decimal.Decimal ["50.00"]
tickets_sold: !!python/object/apply:decimal.Decimal ["1"] tickets_sold: !!python/object/apply:decimal.Decimal ["1"]
@ -162,9 +143,22 @@
invoice_id: "304" invoice_id: "304"
- source: nbpy2017c.html - source: nbpy2017c.html
importer: nbpy2017.Payment2017Importer importer: nbpy2017.InvoiceImporter
expect: expect:
- payee: Python Person C - payee: Python Person C
ledger entry: nbpy2017 invoice ledger entry
date: !!python/object/apply:datetime.date [2017, 10, 5]
amount: !!python/object/apply:decimal.Decimal ["55.00"]
tickets_sold: !!python/object/apply:decimal.Decimal ["1"]
ticket_rate: !!python/object/apply:decimal.Decimal ["21.25"]
shirts_sold: !!python/object/apply:decimal.Decimal ["1"]
shirt_rate: !!python/object/apply:decimal.Decimal ["25.50"]
status: Invoice
currency: USD
invoice_date: !!python/object/apply:datetime.date [2017, 10, 5]
invoice_id: "11"
- payee: Python Person C
ledger entry: nbpy2017 payment ledger entry
date: !!python/object/apply:datetime.date [2017, 10, 5] date: !!python/object/apply:datetime.date [2017, 10, 5]
amount: !!python/object/apply:decimal.Decimal ["55.00"] amount: !!python/object/apply:decimal.Decimal ["55.00"]
tickets_sold: !!python/object/apply:decimal.Decimal ["1"] tickets_sold: !!python/object/apply:decimal.Decimal ["1"]
@ -191,7 +185,7 @@
currency: USD currency: USD
- source: Benevity.csv - source: Benevity.csv
importer: benevity.PaymentImporter importer: benevity.DonationsImporter
expect: expect:
- date: !!python/object/apply:datetime.date [2017, 10, 28] - date: !!python/object/apply:datetime.date [2017, 10, 28]
currency: USD currency: USD

View file

@ -4,10 +4,10 @@ loglevel = critical
signed_currencies = USD signed_currencies = USD
[One] [One]
template patreon cardfees = patreon cardfees ledger entry =
Accrued:Accounts Receivable -{amount} Accrued:Accounts Receivable -{amount}
Expenses:Fees:Credit Card {amount} Expenses:Fees:Credit Card {amount}
template patreon svcfees = patreon servicefees ledger entry =
;SourcePath: {source_abspath} ;SourcePath: {source_abspath}
;SourceName: {source_name} ;SourceName: {source_name}
Accrued:Accounts Receivable -{amount} Accrued:Accounts Receivable -{amount}

View file

@ -27,7 +27,7 @@ def template_vars(payee, amount, currency='USD', date=DATE, other_vars=None):
'currency': currency, 'currency': currency,
'date': date, 'date': date,
'payee': payee, 'payee': payee,
'template': 'template', 'ledger template': 'template',
} }
if other_vars is None: if other_vars is None:
return call_vars return call_vars