mirror of
https://github.com/simonmichael/hledger.git
synced 2024-12-24 19:02:46 +03:00
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()
|