Initial commit
This commit is contained in:
		
						commit
						222ef8b421
					
				
					 2 changed files with 233 additions and 0 deletions
				
			
		
							
								
								
									
										201
									
								
								accounting/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								accounting/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | |||
| import subprocess | ||||
| import logging | ||||
| import time | ||||
| 
 | ||||
| from datetime import datetime | ||||
| from xml.etree import ElementTree | ||||
| from contextlib import contextmanager | ||||
| 
 | ||||
| _log = logging.getLogger(__name__) | ||||
| 
 | ||||
| class Ledger: | ||||
|     def __init__(self, ledger_file=None, ledger_bin=None): | ||||
|         if ledger_file is None: | ||||
|             raise ValueError('ledger_file cannot be None') | ||||
| 
 | ||||
|         self.ledger_bin = ledger_bin or 'ledger' | ||||
|         self.ledger_file = ledger_file | ||||
|         _log.info('ledger file: %s', ledger_file) | ||||
| 
 | ||||
|         self.locked = False | ||||
|         self.ledger_process = None | ||||
| 
 | ||||
|     @contextmanager | ||||
|     def locked_process(self): | ||||
|         if self.locked: | ||||
|             _log.warning('Process is already locked') | ||||
|             for i in range(1, 5): | ||||
|                 if i > 4: | ||||
|                     raise RuntimeError('Ledger process is already locked') | ||||
| 
 | ||||
|                 if not self.locked: | ||||
|                     break | ||||
|                 else: | ||||
|                     _log.info('Waiting for one second... %d/%d', i, 5) | ||||
|                     time.sleep(1) | ||||
| 
 | ||||
| 
 | ||||
|         process = self.get_process() | ||||
| 
 | ||||
|         self.locked = True | ||||
|         _log.debug('lock enabled') | ||||
| 
 | ||||
|         yield process | ||||
| 
 | ||||
|         self.locked = False | ||||
|         _log.debug('lock disabled') | ||||
| 
 | ||||
|     def assemble_arguments(self): | ||||
|         return [ | ||||
|             self.ledger_bin, | ||||
|             '-f', | ||||
|             self.ledger_file, | ||||
|         ] | ||||
| 
 | ||||
|     def init_process(self): | ||||
|         _log.debug('starting ledger process') | ||||
|         self.ledger_process = subprocess.Popen( | ||||
|             self.assemble_arguments(), | ||||
|             stdout=subprocess.PIPE, | ||||
|             stdin=subprocess.PIPE, | ||||
|             stderr=subprocess.PIPE) | ||||
| 
 | ||||
|         # Swallow the banner | ||||
|         with self.locked_process() as p: | ||||
|             self.read_until_prompt(p) | ||||
| 
 | ||||
|         return self.ledger_process | ||||
| 
 | ||||
|     def get_process(self): | ||||
|         return self.ledger_process or self.init_process() | ||||
| 
 | ||||
|     def read_until_prompt(self, p): | ||||
|         output = b'' | ||||
| 
 | ||||
|         while True: | ||||
|             # _log.debug('reading data') | ||||
| 
 | ||||
|             line = p.stdout.read(1)  # XXX: This is a hack | ||||
|             # _log.debug('line: %s', line) | ||||
| 
 | ||||
|             output += line | ||||
| 
 | ||||
|             if b'\n] ' in output: | ||||
|                 _log.debug('found prompt!') | ||||
|                 break | ||||
| 
 | ||||
|         output = output[:-3]  # Cut away the prompt | ||||
| 
 | ||||
|         _log.debug('output: %s', output) | ||||
| 
 | ||||
|         return output | ||||
| 
 | ||||
|     def send_command(self, p, command): | ||||
|         # TODO: Should be extended to handle the locking and return the output | ||||
|         _bytes = p.stdin.write(command + b'\n') | ||||
|         p.stdin.flush() | ||||
| 
 | ||||
|         return _bytes | ||||
| 
 | ||||
|     def bal(self): | ||||
|         output = None | ||||
| 
 | ||||
|         with self.locked_process() as p: | ||||
|             _log.debug('aquired process lock') | ||||
|             self.send_command(p, b'bal --format "%A|%t\\\\n"') | ||||
|             _log.debug('sent command') | ||||
| 
 | ||||
|             output = self.read_until_prompt(p) | ||||
| 
 | ||||
|         if output is None: | ||||
|             raise RuntimeError('bal call returned no output') | ||||
| 
 | ||||
|         accounts = [] | ||||
| 
 | ||||
|         for line in output.split(b'\n'): | ||||
|             name, balance =  line.decode('utf8').split('|') | ||||
| 
 | ||||
|             accounts.append(Account(name=name, balance=balance)) | ||||
| 
 | ||||
|         return accounts | ||||
| 
 | ||||
|     def reg(self): | ||||
|         output = None | ||||
| 
 | ||||
|         with self.locked_process() as p: | ||||
|             _log.debug('aquired process lock') | ||||
|             self.send_command(p, b'xml') | ||||
| 
 | ||||
|             output = self.read_until_prompt(p) | ||||
| 
 | ||||
|         if output is None: | ||||
|             raise RuntimeError('reg call returned no output') | ||||
| 
 | ||||
|         entries = [] | ||||
| 
 | ||||
|         reg_xml = ElementTree.fromstring(output.decode('utf8')) | ||||
| 
 | ||||
|         for transaction in reg_xml.findall('./transactions/transaction'): | ||||
|             date = datetime.strptime(transaction.find('./date').text, | ||||
|                                      '%Y/%m/%d') | ||||
|             payee = transaction.find('./payee').text | ||||
| 
 | ||||
|             postings = [] | ||||
| 
 | ||||
|             for posting in transaction.findall('./postings/posting'): | ||||
|                 account = posting.find('./account/name').text | ||||
|                 amount = posting.find('./post-amount/amount/quantity').text | ||||
|                 symbol = posting.find( | ||||
|                     './post-amount/amount/commodity/symbol').text | ||||
| 
 | ||||
|                 postings.append( | ||||
|                     Posting(account=account, amount=amount, symbol=symbol)) | ||||
| 
 | ||||
|             entries.append( | ||||
|                 Transaction(date=date, payee=payee, postings=postings)) | ||||
| 
 | ||||
|         return entries | ||||
| 
 | ||||
| 
 | ||||
| class Transaction: | ||||
|     def __init__(self, date=None, payee=None, postings=None): | ||||
|         self.date = date | ||||
|         self.payee = payee | ||||
|         self.postings = postings | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return ('<{self.__class__.__name__} {date}' + | ||||
|                 ' {self.payee} {self.postings}').format( | ||||
|                     self=self, | ||||
|                     date=self.date.isoformat()) | ||||
| 
 | ||||
| class Posting: | ||||
|     def __init__(self, account=None, amount=None, symbol=None): | ||||
|         self.account = account | ||||
|         self.amount = amount | ||||
|         self.symbol = symbol | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return ('<{self.__class__.__name__} "{self.account}"' + | ||||
|                 ' {self.symbol} {self.amount}>').format(self=self) | ||||
| 
 | ||||
| 
 | ||||
| class Account: | ||||
|     def __init__(self, name=None, balance=None): | ||||
|         self.name = name | ||||
|         self.balance = balance | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return '<{self.__class__.__name__}: "{self.name}" {self.balance} >'.format( | ||||
|             self=self) | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     ledger = Ledger(ledger_file='non-profit-test-data.ledger') | ||||
|     print(ledger.bal()) | ||||
|     print(ledger.reg()) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     logging.basicConfig(level=logging.DEBUG) | ||||
|     main() | ||||
							
								
								
									
										32
									
								
								non-profit-test-data.ledger
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								non-profit-test-data.ledger
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| 
 | ||||
| 2010/01/01 Kindly T. Donor | ||||
|    Income:Foo:Donation   $-100.00 | ||||
|       ;Invoice: Projects/Foo/Invoices/Invoice20100101.pdf | ||||
|    Assets:Checking       $100.00 | ||||
| 
 | ||||
| 
 | ||||
| 2011/03/15 Another J. Donor | ||||
|    Income:Foo:Donation   $-400.00 | ||||
|       ;Approval: Projects/Foo/earmark-record.txt | ||||
|    Assets:Checking       $400.00 | ||||
| 
 | ||||
| 2011/04/20 (1) Baz Hosting Services, LLC | ||||
|    Expenses:Foo:Hosting   $250.00 | ||||
|       ;Receipt: Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf | ||||
|    Assets:Checking       $-250.00 | ||||
| 
 | ||||
| 2011/05/10 Donation to General Fund | ||||
|    Income:Donation   $-50.00 | ||||
|       ;Invoice: Financial/Invoices/Invoice20110510.pdf | ||||
|    Assets:Checking   $50.00 | ||||
| 
 | ||||
| 2011/04/20 (2) Baz Hosting Services, LLC | ||||
|    Expenses:Blah:Hosting   $250.00 | ||||
|       ;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf | ||||
|       ;Invoice: Projects/Blah/Expenses/hosting/april-invoice.pdf | ||||
|    Assets:Checking       $-250.00 | ||||
|       ;Statement: Financial/BankStuff/bank-statement.pdf | ||||
| 
 | ||||
| 2011-04-25 A transaction with ISO date | ||||
|    Income:Karma              KARMA-10 | ||||
|    Assets:Karma Account      KARMA 10 | ||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Joar Wandborg
						Joar Wandborg