forked from EvergreenCrypto/docker-finance
BTCPay Server v2.2.0 has a new "Invoices" export format that is more complex than the prior "Legacy Invoice Export" format. To help facilitate the transition to the new format, upstream has provided a plugin for backwards compatibility. However, this plugin happens to provide additional columns that must be supported. Additionaly, the "Wallets" format also has new columns with fee information, so the impl now supports tx fees (prior, fees required manual input). The "Wallets" impl also now defaults to a refund expense account for outgoing txs and adds tax rules & documentation for rationale.
275 lines
8.9 KiB
Bash
Executable File
275 lines
8.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# docker-finance | modern accounting for the power-user
|
|
#
|
|
# Copyright (C) 2021-2025 Aaron Fiore (Founder, Evergreen Crypto LLC)
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
# "Libraries"
|
|
#
|
|
|
|
[ -z "$DOCKER_FINANCE_CONTAINER_REPO" ] && exit 1
|
|
source "${DOCKER_FINANCE_CONTAINER_REPO}/src/hledger-flow/lib/lib_preprocess.bash" "$1" "$2"
|
|
source "${DOCKER_FINANCE_CONTAINER_REPO}/src/hledger-flow/lib/lib_utils.bash" || exit 1
|
|
|
|
#
|
|
# Implementation
|
|
#
|
|
|
|
[ -z "$global_year" ] && exit 1
|
|
[ -z "$global_subaccount" ] && exit 1
|
|
|
|
[ -z "$global_in_path" ] && exit 1
|
|
[ -z "$global_out_path" ] && exit 1
|
|
|
|
function btcpayserver::print_warning()
|
|
{
|
|
[ -z "$global_account" ] && lib_utils::die_fatal
|
|
lib_utils::print_warning "${global_account}: '$1' report is not supported"
|
|
}
|
|
|
|
#
|
|
# BTCPay Server v2
|
|
#
|
|
|
|
# NOTE/TODO:
|
|
#
|
|
# TL;DR:
|
|
#
|
|
# - As of v2.2.0, *MUST* install the "Legacy Invoice Export" plugin. TODO: use v2.2.0 format w/out plugin
|
|
#
|
|
# - *MUST* export both "Legacy Invoice" report and "Wallets" report for an accurate balance.
|
|
# * The "Legacy Invoice" report currently does *not* include any outgoing txs (refunds or transfers).
|
|
#
|
|
# WARNING:
|
|
#
|
|
# - If using a watch-ony wallet, consider accounting entirely here (via btcpayserver) or
|
|
# entirely via that other wallet (e.g., electrum). Mixing the two may cause tax accounting issues.
|
|
#
|
|
# CAUTION:
|
|
#
|
|
# - The "Wallets" report contains the txid that the "Payments" report *should* include (but doesn't).
|
|
# * The "Legacy Invoice" report provides both.
|
|
#
|
|
# - The "Wallets" report does *not* contain a description for refunds versus transfers.
|
|
# * This is done in "Payouts" report or "Refunds" report.
|
|
#
|
|
# - The "Payouts" report contains the destination address whereas "Refunds" does not.
|
|
# * "Payouts" places the Invoice ID in the 'Source' field.
|
|
# * "Refunds" places the Invoice ID in the 'InvoiceId' field.
|
|
|
|
# "Legacy Invoice" report
|
|
function btcpayserver::legacy()
|
|
{
|
|
gawk -v global_year="$global_year" -v global_subaccount="$global_subaccount" \
|
|
'{
|
|
if (NR<2 || $1 !~ global_year)
|
|
next
|
|
|
|
# ReceivedDate (w/ local timezone added)
|
|
sub(/ /, "T", $1) # HACK: makes arg-friendly by removing space
|
|
cmd = "date \"+%F %T %z\" --date="$1 | getline date
|
|
printf date OFS
|
|
|
|
printf $2 OFS # StoreId
|
|
printf $3 OFS # OrderId
|
|
printf $4 OFS # InvoiceId
|
|
|
|
# InvoiceCreatedDate (w/ local timezone added)
|
|
sub(/ /, "T", $5)
|
|
cmd = "date \"+%F %T %z\" --date="$5 | getline date
|
|
printf date OFS
|
|
|
|
# InvoiceExpirationDate (w/ local timezone added)
|
|
sub(/ /, "T", $6)
|
|
cmd = "date \"+%F %T %z\" --date="$6 | getline date
|
|
printf date OFS
|
|
|
|
# InvoiceMonitoringDate (w/ local timezone added)
|
|
sub(/ /, "T", $7)
|
|
cmd = "date \"+%F %T %z\" --date="$7 | getline date
|
|
printf date OFS
|
|
|
|
# PaymentId
|
|
# NOTE: BTCPay Server will append the block index as "-N" to the txid
|
|
txid=substr($8, 1, 64); printf txid OFS
|
|
ind=substr($8, 66, 2); printf ind OFS
|
|
|
|
printf $9 OFS # Destination
|
|
printf $10 OFS # PaymentType
|
|
printf $11 OFS # CryptoCode
|
|
|
|
# Paid
|
|
printf("%.8f", $12); printf OFS
|
|
|
|
printf $13 OFS # NetworkFee
|
|
printf $14 OFS # ConversionRate
|
|
printf $15 OFS # PaidCurrency
|
|
printf $16 OFS # InvoiceCurrency
|
|
printf $17 OFS # InvoiceDue
|
|
printf $18 OFS # InvoicePrice
|
|
printf $19 OFS # InvoiceTaxIncluded
|
|
printf $20 OFS # InvoiceTip
|
|
printf $21 OFS # InvoiceSubtotal
|
|
printf $22 OFS # InvoiceItemCode
|
|
|
|
# TODO: if description contains comma(s)?
|
|
printf $23 OFS # InvoiceItemDesc
|
|
|
|
printf $24 OFS # InvoiceFullStatus
|
|
printf $25 OFS # InvoiceStatus
|
|
printf $26 OFS # InvoiceExceptionStatus
|
|
printf $27 OFS # BuyerEmail
|
|
printf $28 OFS # Accounted
|
|
|
|
# WARNING: appears to be always IN (see notes regarding "Wallets" report)
|
|
printf "IN" OFS # Direction
|
|
printf global_subaccount # Subaccount
|
|
|
|
printf "\n"
|
|
}' FS=, OFS=, "$global_in_path"
|
|
}
|
|
|
|
# "Payments" report
|
|
function btcpayserver::payments()
|
|
{
|
|
btcpayserver::print_warning "Payments"
|
|
}
|
|
|
|
# "Payouts" report
|
|
function btcpayserver::payouts()
|
|
{
|
|
btcpayserver::print_warning "Payouts"
|
|
}
|
|
|
|
# "Refunds" report
|
|
function btcpayserver::refunds()
|
|
{
|
|
btcpayserver::print_warning "Refunds"
|
|
}
|
|
|
|
# "Sales" report
|
|
function btcpayserver::sales()
|
|
{
|
|
btcpayserver::print_warning "Sales"
|
|
# TODO: Accounting for this needs more consideration,
|
|
# as these are individual products sold w/ only fiat value given.
|
|
}
|
|
|
|
# "Wallets" report
|
|
function btcpayserver::wallets()
|
|
{
|
|
gawk -v global_year="$global_year" -v global_subaccount="$global_subaccount" \
|
|
'{
|
|
if (NR<2 || $1 !~ global_year)
|
|
next
|
|
|
|
# All "IN"s are removed and handled by legacy invoice
|
|
if ($6 !~ /^-/)
|
|
next
|
|
|
|
# Strip sign from amount (using direction instead)
|
|
sub(/^-/, "", $6)
|
|
|
|
# Date (ReceivedDate w/ local timezone added)
|
|
sub(/ /, "T", $1) # HACK: makes arg-friendly by removing space
|
|
cmd = "date \"+%F %T %z\" --date="$1 | getline date
|
|
printf date OFS
|
|
|
|
printf OFS # (StoreId)
|
|
printf OFS # (OrderId)
|
|
printf $4 OFS # InvoiceId (InvoiceId)
|
|
printf OFS # (InvoiceCreatedDate)
|
|
printf OFS # (InvoiceExpirationDate)
|
|
printf OFS # (InvoiceMonitoringDate)
|
|
|
|
# (PaymentId)
|
|
printf $3 OFS # TransactionId (txid)
|
|
printf OFS # (Block Index)
|
|
|
|
printf OFS # (Destination)
|
|
printf OFS # (PaymentType)
|
|
printf $2 OFS # Crypto (CryptoCode)
|
|
|
|
# BalanceChange (Paid)
|
|
# NOTE: must provide as subtotal, so subtract fee
|
|
printf("%.8f", $6 - $7); printf OFS
|
|
|
|
# Fee (NetworkFee)
|
|
printf("%.8f", $7); printf OFS
|
|
|
|
printf OFS # (ConversionRate)
|
|
|
|
# FeeRate (PaidCurrency)
|
|
printf("%.3f", $8); printf OFS
|
|
|
|
# Rate (XXX) (InvoiceCurrency)
|
|
printf("%.3f", $9); printf OFS
|
|
|
|
printf OFS # (InvoiceDue)
|
|
printf OFS # (InvoicePrice)
|
|
printf OFS # (InvoiceTaxIncluded)
|
|
printf OFS # (InvoiceTip)
|
|
printf OFS # (InvoiceSubtotal)
|
|
printf OFS # (InvoiceItemCode)
|
|
printf OFS # (InvoiceItemDesc)
|
|
printf OFS # (InvoiceFullStatus)
|
|
printf OFS # (InvoiceStatus)
|
|
printf OFS # (InvoiceExceptionStatus)
|
|
printf OFS # (BuyerEmail)
|
|
printf $5 OFS # Confirmed (Accounted)
|
|
|
|
# All "IN"s are removed and handled by legacy invoice
|
|
printf "OUT" OFS # Direction
|
|
printf global_subaccount # Subaccount
|
|
|
|
printf "\n"
|
|
}' FS=, OFS=, "$global_in_path"
|
|
}
|
|
|
|
function btcpayserver::parse()
|
|
{
|
|
lib_preprocess::test_header "ReceivedDate,StoreId,OrderId,InvoiceId,InvoiceCreatedDate,InvoiceExpirationDate,InvoiceMonitoringDate,PaymentId,Destination,PaymentType,CryptoCode,Paid,NetworkFee,ConversionRate,PaidCurrency,InvoiceCurrency,InvoiceDue,InvoicePrice,InvoiceTaxIncluded,InvoiceTip,InvoiceSubtotal,InvoiceItemCode,InvoiceItemDesc,InvoiceFullStatus,InvoiceStatus,InvoiceExceptionStatus,BuyerEmail,Accounted" \
|
|
&& btcpayserver::legacy
|
|
|
|
lib_preprocess::test_header "Date,InvoiceId,OrderId,Category,PaymentMethodId,Confirmed,Address,PaymentCurrency,PaymentAmount,PaymentMethodFee,LightningAddress,InvoiceCurrency,InvoiceCurrencyAmount,Rate" \
|
|
&& btcpayserver::payments
|
|
|
|
lib_preprocess::test_header "Date,Source,State,PaymentType,Currency,Amount,OriginalCurrency,OriginalAmount,Destination" \
|
|
&& btcpayserver::payouts
|
|
|
|
lib_preprocess::test_header "Date,InvoiceId,Currency,Completed,Awaiting,Limit,FullyPaid" \
|
|
&& btcpayserver::refunds
|
|
|
|
lib_preprocess::test_header "Date,InvoiceId,State,AppId,Product,Quantity,CurrencyAmount,Currency" \
|
|
&& btcpayserver::sales
|
|
|
|
# TODO: don't hardcode USD (upstream should make the currency in "Rate (XXX)" into a separate column).
|
|
#lib_preprocess::test_header "Date,Crypto,TransactionId,InvoiceId,Confirmed,BalanceChange,Fee,FeeRate,Rate (USD)" \
|
|
lib_preprocess::test_header "Date,Crypto,TransactionId,InvoiceId,Confirmed,BalanceChange,Fee,FeeRate" \
|
|
&& btcpayserver::wallets
|
|
}
|
|
|
|
function main()
|
|
{
|
|
# WARNING: upstream produces carriage return all EOL
|
|
btcpayserver::parse | sed -e 's:\x0d::g' >"$global_out_path" || lib_utils::catch $?
|
|
}
|
|
|
|
main "$@"
|
|
|
|
# vim: sw=2 sts=2 si ai et
|