Every month begins the same way: download sales CSVs from each store's console, paste them into a spreadsheet. With one app that is fine. As the number of apps grows, the fact that every store names its columns and writes its dates differently starts to cost real time.
One month, the total I had pasted did not match the total shown in the store console. The gap was small, and I could not tell where it had gone. In the end the cause was a refund row that I had removed by hand because it was negative. If you add numbers without any way to check that they are right, the mistake cannot be seen from the numbers alone.
What follows is the procedure I built after that: reduce each store's CSV to four common columns, convert at one fixed month-end rate, and print the raw per-currency totals next to the converted ones so the two can be reconciled. Column names in store CSVs can change, so the script is written on the assumption that you open your own CSV and edit the mapping.
Reduce everything to four columns
For monthly, per-app revenue you need only four things: when (the month), which app, how much, and in which currency. Store CSVs carry many more columns, but these four are enough.
Each store gets a small mapping saying "in this CSV, the date is in that column, the app name in this one". Adding a store means adding one entry.
| Common column | What to watch for |
|---|---|
| Month | Use the first seven characters of the date. 2026/09/05 and 2026-09-05 both become 2026-09 |
| App | Display names can differ between stores. If they do, map them to one name |
| Amount | Strip thousands separators and hold the value as Decimal, not a float. Keep refunds negative |
| Currency | Upper-case it. One CSV I handled had it in lower case |
Holding amounts as Decimal keeps rounding from drifting from month to month after conversion. With a few dozen rows the error is invisible, but later, when a total fails to match, you can no longer trace the cause.
The whole script
Save this as merge_sales.py. It needs only a column mapping and a file of fixed exchange rates. The column names in MAPPING are examples. Open the first line of your own CSV and rewrite them to match.
"""store ごとに形式の違う売上 CSV を共通 4 列に寄せ、アプリ別・月別に集計する。"""
import csv, sys, json
from collections import defaultdict
from decimal import Decimal, InvalidOperation
# Edit this mapping by opening your own CSV (these names are examples)
MAPPING = {
"storeA": {"date": "Date", "app": "App", "amount": "Net", "currency": "Cur"},
"storeB": {"date": "Order Charged Date", "app": "Product", "amount": "Payout", "currency": "Currency"},
}
def load(path, store):
m = MAPPING[store]
rows, bad = [], []
with open(path, newline="", encoding="utf-8-sig") as f:
for n, r in enumerate(csv.DictReader(f), start=2):
try:
rows.append({
"month": r[m["date"]].strip()[:7].replace("/", "-"),
"app": r[m["app"]].strip(),
"amount": Decimal(r[m["amount"]].replace(",", "")),
"currency": r[m["currency"]].strip().upper(),
"store": store,
})
except (KeyError, InvalidOperation) as e:
bad.append((path, n, repr(e))) # never drop silently; keep the line number
return rows, bad
def main(rates_path, *pairs):
rates = json.load(open(rates_path)) # {"2026-09": {"USD": "150.0", "JPY": "1"}}
totals, raw_sum, bad_all = defaultdict(Decimal), defaultdict(Decimal), []
for p in pairs:
store, path = p.split("=", 1)
rows, bad = load(path, store)
bad_all += bad
for r in rows:
rate = Decimal(rates[r["month"]][r["currency"]]) # a missing rate raises KeyError and stops
totals[(r["month"], r["app"])] += r["amount"] * rate
raw_sum[(store, r["currency"])] += r["amount"]
for (month, app), v in sorted(totals.items()):
print(f"{month} {app:<12} {v:>12,.0f} JPY")
print("-- raw totals per store and currency (for reconciliation)")
for (store, cur), v in sorted(raw_sum.items()):
print(f"{store} {cur} {v}")
print(f"-- unreadable rows: {len(bad_all)}")
for b in bad_all: print(" ", *b)
if __name__ == "__main__":
main(*sys.argv[1:])Exchange rates live in one JSON file, one entry per month. Why one fixed rate per month rather than per transaction date is covered below.
{"2026-09": {"USD": "150", "JPY": "1"}}Run it by passing the rates file and store=path pairs.
python3 merge_sales.py rates.json storeA=a.csv storeB=b.csv