about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2015-04-06 10:58:16 +0100
committerAlan Pearce2015-04-06 11:05:01 +0100
commit2d371fdd6b9d805e7d7b35f53c3c626fa1408740 (patch)
tree2255e17ea70f1ceb9a558582f5a1fa5e9f01c5a8
parent115cc1a38c67606909bf3df1cc41d257fa8120ba (diff)
downloadynab-ledger-2d371fdd6b9d805e7d7b35f53c3c626fa1408740.tar.lz
ynab-ledger-2d371fdd6b9d805e7d7b35f53c3c626fa1408740.tar.zst
ynab-ledger-2d371fdd6b9d805e7d7b35f53c3c626fa1408740.zip
Implement Ledger output
-rw-r--r--src/Command/ConvertCommand.php232
-rw-r--r--src/Item/BudgetTransaction.php1
-rw-r--r--src/Item/LedgerPosting.php11
-rw-r--r--src/Item/LedgerTransaction.php11
-rw-r--r--src/Item/RegisterTransaction.php3
5 files changed, 220 insertions, 38 deletions
diff --git a/src/Command/ConvertCommand.php b/src/Command/ConvertCommand.php
index 5a9a484..21f0a03 100644
--- a/src/Command/ConvertCommand.php
+++ b/src/Command/ConvertCommand.php
@@ -9,9 +9,12 @@ use SeekableIterator;
 use ArrayIterator;
 use DateTimeImmutable;
 use NumberFormatter;
+use UnexpectedValueException;
 use CLIFramework\Command;
 use YnabLedger\Item\RegisterTransaction;
 use YnabLedger\Item\BudgetTransaction;
+use YnabLedger\Item\LedgerTransaction;
+use YnabLedger\Item\LedgerPosting;
 
 function convertDate ($currencySymbol) {
     $dateFormat;
@@ -38,6 +41,154 @@ function convertDate ($currencySymbol) {
     };
 }
 
+function convertCleared ($val) {
+    switch ($val) {
+        case 'U': case null:
+            return '';
+        case 'R': case 'C':
+            return '*';
+        default:
+            throw new UnexpectedValueException(
+                sprintf("Found a cleared flag with value '%s', but I don't know what it means", $val)
+            );
+    }
+}
+
+function getAccount ($name, $txn) {
+    static $accounts = [];
+    if (!isset($accounts[$name])) {
+        if ($txn->in >= 0.01 || stripos($name, 'credit') === false) {
+            $accounts[$name] = ['Assets', $name];
+        } else {
+            $accounts[$name] = ['Liabilities', $name ?: end($txn->category)];
+        }
+    }
+    return $accounts[$name];
+}
+
+function toLedger (Generator $transactions) {
+    $transactions->rewind();
+    $accounts = [];
+
+    while ($transactions->valid()) {
+        $inc = true;
+        try {
+            $txn = $transactions->current();
+            $lTxn = new LedgerTransaction;
+            $lTxn->date = $txn->date;
+            $lTxn->state = convertCleared($txn->cleared);
+            $lTxn->payee = $txn->payee;
+            $lTxn->note = $txn->memo;
+
+            if ($txn instanceof BudgetTransaction) {
+                $startDate = $txn->date;
+                do {
+                    $posting = new LedgerPosting;
+                    $posting->currency = $txn->currency;
+                    $posting->amount = $txn->in - $txn->out;
+                    $posting->account = array_merge(['Expenses'], $txn->category);
+                    if ($posting->amount !== 0.00) {
+                        $lTxn->postings[] = $posting;
+                    }
+
+                    $transactions->next();
+                    $txn = $transactions->current();
+                    $inc = false;
+                } while (
+                    $txn instanceof BudgetTransaction &&
+                    $txn->date == $startDate
+                );
+
+                $posting = new LedgerPosting;
+                $posting->account = ['Assets'];
+                $lTxn->postings[] = $posting;
+            } elseif ($txn->payee == 'Starting Balance') {
+                $startDate = $txn->date;
+                do {
+                    $posting = new LedgerPosting;
+                    $posting->currency = $txn->currency;
+                    $posting->account = getAccount($txn->account, $txn);
+                    $posting->amount = $txn->in - $txn->out;
+                    $lTxn->postings[] = $posting;
+
+                    $transactions->next();
+                    $txn = $transactions->current();
+                    $inc = false;
+                } while (
+                    $txn->payee == 'Starting Balance' &&
+                    $txn->date == $startDate
+                );
+
+                $posting = new LedgerPosting;
+                $posting->account = ['Equity', 'Opening Balances'];
+                $lTxn->postings[] = $posting;
+            } elseif (substr($txn->payee, 0, 8) === 'Transfer') {
+                if ($txn->out <= 0.00) {
+                    goto next;
+                }
+                $target = new LedgerPosting;
+                $target->account = getAccount(explode(' : ', $txn->payee)[1], $txn);
+                $target->currency = $txn->currency;
+                $target->amount = $txn->out;
+                $lTxn->postings[] = $target;
+
+                $source = new LedgerPosting;
+                $source->account = getAccount($txn->account, $txn);
+                $lTxn->postings[] = $source;
+
+                $lTxn->payee = 'Transfer';
+            } elseif (substr($lTxn->note, 0, 6) == '(Split') {
+                $payee = $txn->payee;
+                $startDate = $txn->date;
+                $lTxn->note = '';
+
+                $source = new LedgerPosting;
+                $source->account = getAccount($txn->account, $txn);
+
+                do {
+                    $target = new LedgerPosting;
+                    $target->account = array_merge(['Expenses'], $txn->category);
+                    $target->currency = $txn->currency;
+                    $target->amount = $txn->out;
+                    sscanf($txn->memo, "(Split %d/%d) %[^\r]", $i, $k, $target->note);
+                    $lTxn->postings[] = $target;
+
+                    $transactions->next();
+                    $txn = $transactions->current();
+                    $inc = false;
+                } while (
+                    $txn->date == $startDate &&
+                    $txn->payee === $payee &&
+                    substr($txn->memo, 0, 6) == '(Split'
+                );
+                $lTxn->postings[] = $source;
+
+            } else {
+                $target = new LedgerPosting;
+                $target->account = array_merge(['Expenses'], $txn->category);
+                $target->currency = $txn->currency;
+                $target->amount = $txn->out;
+                $lTxn->postings[] = $target;
+
+                $source = new LedgerPosting;
+                $source->account = getAccount($txn->account, $txn);
+                $lTxn->postings[] = $source;
+            }
+            yield $lTxn;
+            next:
+            if ($inc) {
+                $transactions->next();
+            }
+            if (!$transactions->valid()) {
+                return;
+            }
+        } catch (UnexpectedValueException $e) {
+            var_dump($txn);
+            throw $e;
+        }
+    }
+}
+
 function readBudget (SplFileObject $budgetFile) {
     $budgetFile->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);
     $file = new LimitIterator($budgetFile, 1);
@@ -47,25 +198,23 @@ function readBudget (SplFileObject $budgetFile) {
         $txn = new BudgetTransaction;
         $txn->date = DateTimeImmutable::createFromFormat('d F Y', '1 ' . $row[0])->setTime(0, 0, 0);
         $txn->category = explode(':', $row[1], 2);
-        $txn->in = $fmt->parseCurrency($row[4], $curr);
-        $txn->out = $fmt->parseCurrency($row[5], $curr);
+        $txn->in = $fmt->parseCurrency($row[4], $txn->currency);
+        $txn->out = $fmt->parseCurrency($row[5], $txn->currency);
         $txn->balance = $row[6];
-        if (!in_array($row[2], $txn->category, true)) {
-            array_unshift($txn->category, $row[2]);
-        }
+        // if (!in_array($row[2], $txn->category, true)) {
+        //     array_unshift($txn->category, $row[2]);
+        // }
         yield $txn;
     }
 }
 
-function readRegister (SplFileObject $registerFile, $locale) {
+function readRegister (SplFileObject $registerFile, NumberFormatter $fmt) {
     $registerFile->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);
 
     $iterator = new LimitIterator($registerFile, 1);
     $iterator->rewind();
     $convertDate = convertDate($iterator->current()[9]);
 
-    $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
-
     $txns = [];
     foreach ($iterator as $i => $row) {
         // "Account","Flag","Check Number","Date","Payee","Category","Master Category","Sub Category","Memo","Outflow","Inflow","Cleared","Running Balance"
@@ -73,44 +222,48 @@ function readRegister (SplFileObject $registerFile, $locale) {
         $txn->account = $row[0];
         $txn->date = $convertDate($row[3])->setTime(0, 0, 0);
         $txn->payee = $row[4];
-        $txn->category = explode(':', $row[5]);
-        if (!in_array($row[6], $txn->category, true)) {
-            array_unshift($txn->category, $row[6]);
-        }
-        $txn->memo = $row[8];
-        $txn->out = $fmt->parseCurrency($row[9], $curr);
-        $txn->in = $fmt->parseCurrency($row[10], $curr);
-        $txn->cleared = ($row[11] !== 'U');
+        $txn->category = array_map('trim', explode(':', $row[5]));
+        // if (!in_array($row[6], $txn->category, true)) {
+        //     array_unshift($txn->category, $row[6]);
+        // }
+        $txn->memo = trim($row[8]);
+        $txn->out = $fmt->parseCurrency($row[9], $txn->currency);
+        $txn->in = $fmt->parseCurrency($row[10], $txn->currency);
+        $txn->cleared = $row[11];
         $txn->line = $i;
         $txns[] = $txn;
     }
 
-    // Sort by date, then promote income and fall back on line in file
+    // Sort by date, group splits, promote income and fall back on line in file
     usort($txns,
           function ($a, $b) use ($convertDate) {
               return strcmp($a->date->format('U'), $b->date->format('U'))
-                  ?: ($b->in < 0.01
-                      ? 0 : $a->line - $b->line);
+                  ?: ((in_array('Split', [substr($a->memo, 1, 5), substr($b->memo, 1, 5)]) ?
+                       strcmp($a->memo, $b->memo) : false)
+                      ?: ($b->out > 0.01 // Include empty starting balance transactions
+                          ? 0 : $a->line - $b->line));
           }
     );
     foreach ($txns as $txn) yield $txn;
 }
 
-function multiRead (Generator $budgetFile, Generator $registerFile) {
-    $budgetFile->rewind();
-    foreach ($registerFile as $rTxn) {
+function multiRead (SplFileObject $budgetFile, SplFileObject $registerFile, NumberFormatter $fmt) {
+    $budget = readBudget($budgetFile);
+    $register = readRegister($registerFile, $fmt);
+    $budget->rewind();
+    foreach ($register as $rTxn) {
         if ($rTxn->payee !== 'Starting Balance' &&
             $rTxn->category[1] !== 'Available this month') {
-            if ($budgetFile->valid()) {
-                $bTxn = $budgetFile->current();
+            if ($budget->valid()) {
+                $bTxn = $budget->current();
                 $bDate = $bTxn->date;
                 if ($bTxn->date < $rTxn->date) {
-                    sleep(2);
                     do {
+                        $bTxn->date = $rTxn->date;
                         yield $bTxn;
 
-                        $budgetFile->next();
-                        $bTxn = $budgetFile->current();
+                        $budget->next();
+                        $bTxn = $budget->current();
                     } while ($bTxn->date == $bDate);
                 }
             }
@@ -137,16 +290,21 @@ class ConvertCommand extends Command {
     }
 
     public function execute ($budgetFile, $registerFile) {
-        $locale = 'en_GB';
-        $budget = readBudget(new SplFileObject($budgetFile));
-        $register = readRegister(new SplFileObject($registerFile), $locale);
-        foreach (multiRead($budget, $register) as $txn) {
-            printf("%-8s %-10s\t%-30s %s\n",
-                   $txn instanceof RegisterTransaction ? 'Register' : 'Budget',
-                   $txn->date->format('Y-m-d'),
-                   implode(':', $txn->category),
-                   isset($txn->payee) ? $txn->payee : ''
-            );
+        $fmt = new NumberFormatter('en_GB', NumberFormatter::CURRENCY);
+        $export = toLedger(multiRead(new SplFileObject($budgetFile), new SplFileObject($registerFile), $fmt));
+
+        foreach ($export as $txn) {
+            echo "{$txn->date->format('Y-m-d')} $txn->payee"
+                , !empty($txn->note) ? "  ; $txn->note" : ""
+                , PHP_EOL;
+            foreach ($txn->postings as $posting) {
+                echo "  " . implode(':', $posting->account);
+                if ($posting->currency !== null) {
+                    echo "  {$fmt->formatCurrency($posting->amount, $posting->currency)}";
+                }
+                echo !empty($posting->note) ? "  ; $posting->note" : "", PHP_EOL;
+            }
+            echo PHP_EOL;
         }
     }
 }
diff --git a/src/Item/BudgetTransaction.php b/src/Item/BudgetTransaction.php
index e4899f9..e073ed2 100644
--- a/src/Item/BudgetTransaction.php
+++ b/src/Item/BudgetTransaction.php
@@ -7,4 +7,5 @@ class BudgetTransaction {
     public $category = [];
     public $in;
     public $out;
+    public $currency;
 }
diff --git a/src/Item/LedgerPosting.php b/src/Item/LedgerPosting.php
new file mode 100644
index 0000000..79aeb49
--- /dev/null
+++ b/src/Item/LedgerPosting.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace YnabLedger\Item;
+
+class LedgerPosting {
+    public $account = [];
+    public $isVirtual = false;
+    public $amount;
+    public $currency;
+    public $note;
+}
diff --git a/src/Item/LedgerTransaction.php b/src/Item/LedgerTransaction.php
new file mode 100644
index 0000000..ebe26e8
--- /dev/null
+++ b/src/Item/LedgerTransaction.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace YnabLedger\Item;
+
+class LedgerTransaction {
+    public $date;
+    public $state;
+    public $payee;
+    public $note;
+    public $postings = [];
+}
diff --git a/src/Item/RegisterTransaction.php b/src/Item/RegisterTransaction.php
index b676640..3c9842a 100644
--- a/src/Item/RegisterTransaction.php
+++ b/src/Item/RegisterTransaction.php
@@ -11,5 +11,6 @@ class RegisterTransaction {
     public $memo;
     public $out;
     public $in;
-    public $cleared = false;
+    public $currency;
+    public $cleared;
 }