264 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| 
 | |
| __version__ = "1.0"
 | |
| __author__ = "Simon Michael"
 | |
| 
 | |
| VERSION = f"%prog {__version__}, by {__author__} 2021; part of the hledger project."
 | |
| USAGE = """%prog [options] [JSONFILE|-] [CSVFILE|-]
 | |
| 
 | |
| Reads Paypal API's transaction history JSON from the first file or
 | |
| stdin, writes Paypal-ish CSV (similar to manually downloaded CSV) to
 | |
| the second file or stdout.
 | |
| 
 | |
| The goal is that you can use the same hledger CSV rules file to
 | |
| process either CSV. So if you get tired of downloading CSV manually,
 | |
| you can set up Paypal API access, download transactions as JSON,
 | |
| convert that to CSV with this script, and keep importing it with
 | |
| hledger without having to redo your paypal CSV rules.
 | |
| 
 | |
| Limitations:
 | |
| 
 | |
| - the Type column will have the short T-Codes rather than the long Descriptions
 | |
|   from https://developer.paypal.com/docs/reports/reference/tcodes
 | |
| 
 | |
| - the Status column will have the single-letter codes from 
 | |
|   https://developer.paypal.com/docs/api/transaction-search/v1/#transactions
 | |
|   rather than the human-friendly values from 
 | |
|   https://developer.paypal.com/docs/reports/online-reports/activity-download
 | |
| 
 | |
| Requirements: python 3, paypaljson for downloading JSON.
 | |
| 
 | |
| Expects paypal transactions JSON, as from 
 | |
| https://developer.paypal.com/docs/api/transaction-search/v1/#transactions,
 | |
| containing most of the following items:
 | |
| 
 | |
| transaction_details[].transaction_info.paypal_account_id
 | |
| transaction_details[].transaction_info.transaction_id
 | |
| transaction_details[].transaction_info.paypal_reference_id
 | |
| transaction_details[].transaction_info.paypal_reference_id_type
 | |
| transaction_details[].transaction_info.transaction_event_code
 | |
| transaction_details[].transaction_info.transaction_initiation_date
 | |
| transaction_details[].transaction_info.transaction_updated_date
 | |
| transaction_details[].transaction_info.transaction_amount.currency_code
 | |
| transaction_details[].transaction_info.transaction_amount.value
 | |
| transaction_details[].transaction_info.transaction_status
 | |
| transaction_details[].transaction_info.ending_balance.currency_code
 | |
| transaction_details[].transaction_info.ending_balance.value
 | |
| transaction_details[].transaction_info.available_balance.currency_code
 | |
| transaction_details[].transaction_info.available_balance.value
 | |
| transaction_details[].transaction_info.invoice_id
 | |
| transaction_details[].transaction_info.protection_eligibility
 | |
| transaction_details[].payer_info.account_id
 | |
| transaction_details[].payer_info.email_address
 | |
| transaction_details[].payer_info.address_status
 | |
| transaction_details[].payer_info.payer_status
 | |
| transaction_details[].payer_info.payer_name.alternate_full_name
 | |
| transaction_details[].payer_info.country_code
 | |
| transaction_details[].shipping_info.name
 | |
| transaction_details[].cart_info.item_details[].item_name
 | |
| transaction_details[].cart_info.item_details[].item_description
 | |
| transaction_details[].cart_info.item_details[].item_quantity
 | |
| transaction_details[].cart_info.item_details[].item_unit_price.currency_code
 | |
| transaction_details[].cart_info.item_details[].item_unit_price.value
 | |
| transaction_details[].cart_info.item_details[].item_amount.currency_code
 | |
| transaction_details[].cart_info.item_details[].item_amount.value
 | |
| transaction_details[].cart_info.item_details[].total_item_amount.currency_code
 | |
| transaction_details[].cart_info.item_details[].total_item_amount.value
 | |
| transaction_details[].cart_info.item_details[].invoice_number
 | |
| transaction_details[].store_info
 | |
| transaction_details[].auction_info
 | |
| transaction_details[].incentive_info 
 | |
| 
 | |
| These are also in paypal transactions JSON, but ignored by this script:
 | |
| 
 | |
| account_number
 | |
| start_date
 | |
| end_date
 | |
| last_refreshed_datetime
 | |
| page
 | |
| total_items
 | |
| total_pages
 | |
| links[].href
 | |
| links[].rel
 | |
| links[].method
 | |
| 
 | |
| Examples: 
 | |
| paypaljson | paypaljson2csv | hledger -f csv:- --rules-file paypal.csv.rules print
 | |
| paypaljson | paypaljson2csv | hledger -f csv:- --rules-file paypal.csv.rules activity --daily
 | |
| paypaljson | paypaljson2csv paypal.csv && hledger import paypal.csv [--dry-run]  # save as a file to remember transactions seen
 | |
| 
 | |
| Sample output:
 | |
| 
 | |
| "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Item Title","Item ID","Reference Txn ID","Receipt ID","Balance","Note"
 | |
| "05/21/2021","00:25:50","HST","JetBrains Americas, Inc.","T0003","S","USD","-53.00","0.00","-53.00","","paypal.us@jetbrains.com","6XG491707Y653863W","1xPhpStorm","","B-63G006809C718195T","","-53.00",""
 | |
| "05/21/2021","00:25:50","HST","","T0300","S","USD","53.00","0.00","53.00","","","724308646Y347530R","1xPhpStorm","","6XG491707Y653863W","","0.00",""\
 | |
| """
 | |
| 
 | |
| from pprint import pprint as pp
 | |
| import csv
 | |
| import datetime
 | |
| import decimal
 | |
| import json
 | |
| import optparse
 | |
| import re
 | |
| # import subprocess
 | |
| import sys
 | |
| # import tempfile
 | |
| # import warnings
 | |
| 
 | |
| def parse_options():
 | |
|     parser = optparse.OptionParser(usage=USAGE, version=VERSION)
 | |
|     opts, args = parser.parse_args()
 | |
|     if len(args) > 2:
 | |
|         parser.print_help()
 | |
|         sys.exit()
 | |
|     return opts, args
 | |
| 
 | |
| def main():
 | |
|     opts, args = parse_options()
 | |
|     out = open(args[1],'w') if len(args) > 1 and not args[1]=='-' else sys.stdout
 | |
|     csvwriter = csv.writer(out, quoting=csv.QUOTE_ALL)
 | |
|     with open(args[0],'r') if len(args) > 0 and not args[0]=='-' else sys.stdin as inp:
 | |
| 
 | |
|         j = json.load(inp)
 | |
|         txns = j['transaction_details']
 | |
| 
 | |
|         csvwriter.writerow([
 | |
|             "Date",
 | |
|             "Time",
 | |
|             "TimeZone",
 | |
|             "Name",
 | |
|             "Type",
 | |
|             "Status",
 | |
|             "Currency",
 | |
|             "Gross",
 | |
|             "Fee",
 | |
|             "Net",
 | |
|             "From Email Address",
 | |
|             "To Email Address",
 | |
|             "Transaction ID",
 | |
|             "Item Title",
 | |
|             "Item ID",
 | |
|             "Reference Txn ID",
 | |
|             "Receipt ID",
 | |
|             "Balance",
 | |
|             "Note"
 | |
|             ])
 | |
| 
 | |
|         for t in txns:
 | |
|             txninfo       = t['transaction_info']
 | |
|             payerinfo     = t['payer_info']
 | |
|             shippinginfo  = t['shipping_info']
 | |
|             cartinfo      = t['cart_info']
 | |
|             storeinfo     = t['store_info']
 | |
|             auctioninfo   = t['auction_info']
 | |
|             incentiveinfo = t['incentive_info']
 | |
| 
 | |
|             localdt  = datetime.datetime.strptime(txninfo['transaction_initiation_date'], "%Y-%m-%dT%H:%M:%S%z").astimezone()
 | |
| 
 | |
|             grossamt = decimal.Decimal(txninfo.get('transaction_amount',{}).get('value','0.00'))
 | |
|             feeamt   = decimal.Decimal(txninfo.get('fee_amount',{}).get('value','0.00'))
 | |
|             netamt   = grossamt + feeamt
 | |
| 
 | |
|             # for type, csv shows the long Description from
 | |
|             # https://developer.paypal.com/docs/reports/reference/tcodes,
 | |
|             # json shows the short T-Code. There are too many, keep the T-Code for now.
 | |
|             # csv rules files will have to be aware.
 | |
|             ttype = txninfo.get('transaction_event_code','')
 | |
| 
 | |
|             # for status, csv shows the human-friendly values from 
 | |
|             # https://developer.paypal.com/docs/reports/online-reports/activity-download/#download-data-fields -> Status:
 | |
|             #  Possible values for all activity:
 | |
|             #   Completed
 | |
|             #   Denied
 | |
|             #   Reversed
 | |
|             #   Pending
 | |
|             #   Active
 | |
|             #   Expired
 | |
|             #   Removed
 | |
|             #   Unverified
 | |
|             #   Voided
 | |
|             #   Processing
 | |
|             #   Created
 | |
|             #   Canceled
 | |
|             #  Possible additional values are supported for invoice activity:
 | |
|             #   Error
 | |
|             #   Draft
 | |
|             #   Unpaid
 | |
|             #   Paid
 | |
|             #   Unpaid (sent)
 | |
|             #   Marked as paid
 | |
|             #   Marked as refunded
 | |
|             #   Refunded
 | |
|             #   Partially refunded
 | |
|             #   Scheduled
 | |
|             #   Partially paid
 | |
|             #   Payment pending
 | |
|             # json shows the single-letter codes from 
 | |
|             # https://developer.paypal.com/docs/api/transaction-search/v1/#transactions -> transaction_status:
 | |
|             #  D	- PayPal or merchant rules denied the transaction.
 | |
|             #  P	- The transaction is pending. The transaction was created but waits for another payment process to complete, such as an ACH transaction, before the status changes to S.
 | |
|             #  S	- The transaction successfully completed without a denial and after any pending statuses.
 | |
|             #  V	- A successful transaction was reversed and funds were refunded to the original sender.
 | |
|             # Keep the short codes for now, csv rules files will have to be aware.
 | |
|             tstatus = txninfo.get('transaction_status','')
 | |
|             
 | |
|             # for Item Title, use transaction_subject if any, otherwise first item title if any
 | |
|             if 'transaction_subject' in txninfo:
 | |
|                 item_title = txninfo.get('transaction_subject','')
 | |
|                 item_id    = ''
 | |
|             else:
 | |
|                 items = cartinfo.get('item_details',[])
 | |
|                 if len(items)>0:
 | |
|                     item_title = items[0].get('item_name','')
 | |
|                     item_id    = items[0].get('item_id','')
 | |
|                 else:
 | |
|                     item_title = ''
 | |
|                     item_id    = ''
 | |
| 
 | |
|             # in the notes below, "csv" refers to CSV downloaded manually, "json" refers to JSON downloaded from the API
 | |
|             csvwriter.writerow([
 | |
|                 # Date
 | |
|                 localdt.strftime('%m/%d/%Y'),
 | |
|                 # Time
 | |
|                 localdt.strftime('%H:%M:%S'),
 | |
|                 # TimeZone (should be downloader's local timezone)
 | |
|                 localdt.strftime('%Z'),
 | |
|                 # Name
 | |
|                 payerinfo.get('payer_name',{}).get('alternate_full_name',''),
 | |
|                 # Type
 | |
|                 ttype,
 | |
|                 # Status
 | |
|                 tstatus,
 | |
|                 # Currency
 | |
|                 txninfo.get('transaction_amount',{}).get('currency_code',''),
 | |
|                 # Gross
 | |
|                 str(grossamt),
 | |
|                 # Fee
 | |
|                 str(feeamt),
 | |
|                 # Net
 | |
|                 str(netamt),
 | |
|                 # From Email Address
 | |
|                 # csv often mentions account owner's email here, json does not
 | |
|                 payerinfo.get('email_address','') if grossamt > 0 else '',
 | |
|                 # To Email Address
 | |
|                 # csv often mentions account owner's email here, json does not
 | |
|                 payerinfo.get('email_address','') if grossamt <= 0 else '',
 | |
|                 # Transaction ID
 | |
|                 txninfo.get('transaction_id',''),
 | |
|                 # Item Title (of first item)
 | |
|                 item_title,
 | |
|                 # Item ID (if any)
 | |
|                 item_id,
 | |
|                 # Reference Txn ID
 | |
|                 txninfo.get('paypal_reference_id',''),
 | |
|                 # Receipt ID
 | |
|                 txninfo.get('receipt_id',''),
 | |
|                 # Balance
 | |
|                 txninfo.get('ending_balance',{}).get('value',''),
 | |
|                 # Note
 | |
|                 txninfo.get('transaction_note','')
 | |
|             ])
 | |
| 
 | |
| if __name__ == "__main__": main()
 |