class RECADDR(Aggregate): """ OFX tax extensions section 2.2.8 """ recname1 = String(32, required=True) recname2 = String(32) addr1 = String(32, required=True) addr2 = String(32) addr3 = String(32) city = String(32, required=True) state = String(5, required=True) postalcode = String(11, required=True) countrystring = String(32) phone = String(32)
class STPCHKNUM(Aggregate, Origcurrency): """ OFX section 11.6.1.2.1 """ checknum = String(12, required=True) name = String(32) dtuser = DateTime() trnamt = Decimal() chkstatus = OneOf("0", "1", "100", "101", required=True) chkerror = String(255) currency = SubAggregate(CURRENCY) origcurrency = SubAggregate(ORIGCURRENCY) optionalMutexes = [("currency", "origcurrency")]
class ACCTINFO(Aggregate): """ OFX section 8.5.3 The text description is a little ambiguous. Here's what the schema says: <xsd:sequence> <xsd:element name="DESC" type="ofx:ShortMessageType" minOccurs="0"/> <xsd:element name="PHONE" type="ofx:PhoneType" minOccurs="0"/> <xsd:sequence maxOccurs="unbounded"> <xsd:choice> <xsd:element name="BANKACCTINFO" type="ofx:BankAccountInfo"/> <xsd:element name="CCACCTINFO" type="ofx:CreditCardAccountInfo"/> <xsd:element name="BPACCTINFO" type="ofx:BillPaymentAccountInfo"/> <xsd:element name="INVACCTINFO" type="ofx:InvestmentAccountInfo"/> <xsd:element name="PRESACCTINFO" type="ofx:PresentmentAccountInfo"/> </xsd:choice> </xsd:sequence> </xsd:sequence> """ desc = String(80) phone = String(32) bankacctinfo = ListAggregate(BANKACCTINFO) ccacctinfo = ListAggregate(CCACCTINFO) bpacctinfo = ListAggregate(BPACCTINFO) invacctinfo = ListAggregate(INVACCTINFO) # presacctinfo = ListAggregate(PRESACCTINFO) @classmethod def validate_args(cls, *args, **kwargs): # Must contain at least one <xxxACCTINFO> if len(args) == 0: msg = "{} must contain at least one of {}" raise ValueError(msg.format(cls.__name__, cls.listaggregates.keys())) # For a given service xxx, there can be at most one <xxxACCTINFO> # returned. For example, you cannot return two <BANKACCTINFO> # aggregates. sortKey = operator.attrgetter("__class__.__name__") args_copy = sorted(args, key=sortKey) for tag, group in itertools.groupby(args_copy, key=sortKey): if len(list(group)) > 1: msg = "{} contains multiple {} aggregates" raise ValueError(msg.format(cls.__name__, tag)) super().validate_args(*args, **kwargs) def __repr__(self): return "<{} desc='{}' phone='{}' len={}>".format( self.__class__.__name__, self.desc, self.phone, len(self) )
class STMTTRN(Aggregate, Origcurrency): """OFX section 11.4.3""" trntype = OneOf(*TRNTYPES, required=True) dtposted = DateTime(required=True) dtuser = DateTime() dtavail = DateTime() trnamt = Decimal(required=True) fitid = String(255, required=True) correctfitid = String(255) correctaction = OneOf("REPLACE", "DELETE") srvrtid = String(10) checknum = String(12) refnum = String(32) sic = Integer() payeeid = String(12) name = NagString(32) payee = SubAggregate(PAYEE) extdname = String(100) bankacctto = SubAggregate(BANKACCTTO) ccacctto = SubAggregate(CCACCTTO) memo = String(255) imagedata = Unsupported() currency = SubAggregate(CURRENCY) origcurrency = SubAggregate(ORIGCURRENCY) inv401ksource = OneOf(*INV401KSOURCES) optionalMutexes = [ ["name", "payee"], ["ccacctto", "bankacctto"], ["currency", "origcurrency"], ]
class SECINFO(Aggregate): """OFX Section 13.8.5.1""" secid = SubAggregate(SECID, required=True) # FIs abuse SECNAME/TICKER # Relaxing the length constraints from the OFX spec does little harm secname = NagString(120, required=True) ticker = NagString(32) fiid = String(32) rating = String(10) unitprice = Decimal() dtasof = DateTime() currency = SubAggregate(CURRENCY) memo = String(255)
class PAYERADDR(Aggregate): """OFX tax extensions section 2.2.7""" payername1 = String(32, required=True) payername2 = String(32) addr1 = String(32, required=True) addr2 = String(32) addr3 = String(32) city = String(32, required=True) state = String(5, required=True) postalcode = String(11, required=True) phone = String(32)
class SELLMF(Aggregate): """ OFX section 13.9.2.4.4 """ invsell = SubAggregate(INVSELL, required=True) selltype = OneOf(*SELLTYPES, required=True) avgcostbasis = Decimal() relfitid = String(255)
class PMTINFO(Aggregate): """ OFX Section 12.5.2 """ bankacctfrom = SubAggregate(BANKACCTFROM, required=True) trnamt = Decimal(required=True) payeeid = String(12) payee = SubAggregate(PAYEE) payeelstid = String(12) bankacctto = SubAggregate(BANKACCTTO) extdpmt = ListItem(EXTDPMT) payacct = String(32, required=True) dtdue = DateTime(required=True) memo = String(255) billrefinfo = String(80) billpubinfo = SubAggregate(BILLPUBINFO) requiredMutexes = [("payeeid", "payee")]
class CLOSUREOPT(INVTRAN, SECID): optaction = OneOf('EXERCISE', 'ASSIGN', 'EXPIRE') units = Decimal(required=True) shperctrct = Integer(required=True) subacctsec = OneOf(*INVSUBACCTS, required=True) relfitid = String(255) gain = Decimal()
class OPTINFO(SECINFO): opttype = OneOf('CALL', 'PUT', required=True) strikeprice = Decimal(required=True) dtexpire = DateTime(required=True) shperctrct = Integer(required=True) assetclass = OneOf(*ASSETCLASSES) fiassetclass = String(32) def __init__(self, elem): """ Strip SECID of underlying so it doesn't overwrite SECID of option during _flatten() """ # Do all XPath searches before removing nodes from the tree # which seems to mess up the DOM in Python3 and throw an # AttributeError on subsequent searches. secid = elem.find('./SECID') if secid is not None: # A <SECID> aggregate referring to the security underlying the # option is, in general, *not* going to be contained in <SECLIST> # (because you don't necessarily have a position in the underlying). # Since the <SECID> for the underlying only gives us fields for # (uniqueidtype, uniqueid) we can't really go ahead and use this # information to create a corresponding SECINFO instance (since we # lack information about the security subclass). It's unclear that # the SECID of the underlying is really needed for anything, so we # disregard it. elem.remove(secid) super(OPTINFO, self).__init__(elem)
class DISCOUNT(Aggregate): """OFX Section 12.5.2.3""" dscrate = Decimal(required=True) dscamt = Decimal(required=True) dscdate = DateTime() dscdesc = String(80, required=True)
class CCSTMTRS(Aggregate): """ OFX section 11.4.3.2 """ curdef = OneOf(*CURRENCY_CODES, required=True) ccacctfrom = SubAggregate(CCACCTFROM, required=True) banktranlist = SubAggregate(BANKTRANLIST) banktranlistp = Unsupported() ledgerbal = SubAggregate(LEDGERBAL, required=True) availbal = SubAggregate(AVAILBAL) cashadvbalamt = Decimal() intratepurch = Decimal() intratecash = Decimal() intratexfer = Decimal() rewardinfo = SubAggregate(REWARDINFO) ballist = SubAggregate(BALLIST) mktginfo = String(360) @property def account(self): return self.ccacctfrom @property def transactions(self): return self.banktranlist @property def balance(self): return self.ledgerbal
class INVSTMTRS(Aggregate): """ OFX section 13.9.2.1 """ dtasof = DateTime(required=True) curdef = OneOf(*CURRENCY_CODES, required=True) invacctfrom = SubAggregate(INVACCTFROM, required=True) invtranlist = SubAggregate(INVTRANLIST) invposlist = SubAggregate(INVPOSLIST) invbal = SubAggregate(INVBAL) invoolist = SubAggregate(INVOOLIST) mktginfo = String(360) inv401kbal = SubAggregate(INV401KBAL) inv401k = SubAggregate(INV401K) @property def account(self): return self.invacctfrom @property def transactions(self): return self.invtranlist @property def positions(self): return self.invposlist @property def balances(self): return self.invbal
class RECINTERMODRQ(Aggregate): """ OFX section 11.10.5.1 """ recsrvrtid = String(10, required=True) recurrinst = SubAggregate(RECURRINST, required=True) interrq = SubAggregate(INTERRQ, required=True) modpending = Bool(required=True)
class RECINTRAMODRS(Aggregate): """ OFX section 11.10.2.2 """ recsrvrtid = String(10, required=True) recurrinst = SubAggregate(RECURRINST, required=True) intrars = SubAggregate(INTRARS, required=True) modpending = Bool(required=True)
class OO(Aggregate): """ OFX section 13.9.2.5.1 - General open order aggregate """ fitid = String(255, required=True) srvrtid = String(10) secid = SubAggregate(SECID, required=True) dtplaced = DateTime(required=True) units = Decimal(required=True) subacct = OneOf(*INVSUBACCTS, required=True) duration = OneOf('DAY', 'GOODTILCANCEL', 'IMMEDIATE', required=True) restriction = OneOf('ALLORNONE', 'MINUNITS', 'NONE', required=True) minunits = Decimal() limitprice = Decimal() stopprice = Decimal() memo = String(255) currency = SubAggregate(CURRENCY) inv401ksource = OneOf(*INV401KSOURCES)
class INV401K(Aggregate): """ OFX section 13.9.3 """ employername = String(32, required=True) planid = String(32) planjoindate = DateTime() employercontactinfo = String(255) brokercontactinfo = String(255) deferpctpretax = Decimal() deferpctaftertax = Decimal() matchinfo = SubAggregate(MATCHINFO) contribinfo = SubAggregate(CONTRIBINFO) currentvestpct = Decimal() vestinfo = ListItem(VESTINFO) loaninfo = ListItem(LOANINFO) inv401ksummary = SubAggregate(INV401KSUMMARY)
class TAX1099R_V100(Aggregate): """OFX tax extensions section 2.2.10""" srvrtid = String(10, required=True) taxyear = Integer(4, required=True) void = Bool() grossdist = Bool() taxamt = Decimal() taxamtnd = Decimal() totaldist = Bool() capgain = Decimal() fedtaxwh = Decimal() empcontins = Decimal() netunapmp = Decimal() nonempcomp = Decimal() distcode = String(1, required=True) irasepsimp = Bool() annctrctdist = Decimal() annctrctper = Decimal() pertodist = Decimal() totempcont = Decimal() amtallocableirr = Decimal() firstyeardesigroth = Integer(4) sttaxwhagg = ListAggregate(STTAXWHAGG) lcltaxwhagg = ListAggregate(LCLTAXWHAGG) payeraddr = SubAggregate(PAYERADDR, required=True) payerid = String(32, required=True) recaddr = SubAggregate(RECADDR) recid = String(32) recacct = String(32) fatca = Bool() dtbenefitpmt = DateTime() @classmethod def validate_args(cls, *args, **kwargs): # "[IRASEPSIMP] is required if any of the following tags are present in # the 1099R aggregate: GROSSDIST, TAXAMT, FEDTAXWH, STTAXWH, # or LCLTAXWH" has_irasepsimp = "irasepsimp" in kwargs for tag in ("grossdist", "taxamt", "fedtaxwh", "sttaxwh", "lcltaxwh"): if tag in kwargs and not has_irasepsimp: msg = ( "{}.__init__(): irasepsimp must also be provided if {} is provided" ) raise ValueError(msg.format(cls.__name__, tag)) super().validate_args(*args, **kwargs)
class EXTBANKDESC(Aggregate): """ OFX section 11.9.1.1.2 """ name = String(32, required=True) bankid = String(9, required=True) addr1 = String(32, required=True) addr2 = String(32) addr3 = String(32) city = String(32, required=True) state = String(5, required=True) postalcode = String(11, required=True) country = OneOf(*COUNTRY_CODES) phone = String(32)
class STPCHKRS(Aggregate): """OFX section 11.6.1.1""" curdef = OneOf(*CURRENCY_CODES, required=True) bankacctfrom = SubAggregate(BANKACCTFROM, required=True) stpchknum = ListAggregate(STPCHKNUM) fee = Decimal(required=True) feemsg = String(80, required=True)
class WIRERS(Aggregate): """ OFX section 11.9.1.2 """ curdef = OneOf(*CURRENCY_CODES, required=True) srvrtid = String(10, required=True) bankacctfrom = SubAggregate(BANKACCTFROM, required=True) wirebeneficiary = SubAggregate(WIREBENEFICIARY, required=True) wiredestbank = SubAggregate(WIREDESTBANK) trnamt = Decimal(required=True) dtdue = DateTime() payinstruct = String(255) dtxferprj = DateTime() dtposted = DateTime() fee = Decimal() confmsg = String(255) optionalMutexes = [("dtxferprj", "dtposted")]
class RECPMTMODRS(Aggregate): """ OFX Section 12.7.2.1 """ recsrvrtid = String(10, required=True) recurrinst = SubAggregate(RECURRINST, required=True) pmtinfo = SubAggregate(PMTINFO, required=True) initialamt = Decimal() finalamt = Decimal() modpending = Bool(required=True)
class PROFTRNRS(Aggregate): trnuid = String(36, required=True) status = SubAggregate(STATUS) profrs = SubAggregate(PROFRS) @property def profile(self): return self.profrs
class SELLOPT(Aggregate): """ OFX section 13.9.2.4.4 """ invsell = SubAggregate(INVSELL, required=True) optselltype = OneOf(*OPTSELLTYPES, required=True) shperctrct = Integer(required=True) relfitid = String(255) reltype = OneOf('SPREAD', 'STRADDLE', 'NONE', 'OTHER') secured = OneOf('NAKED', 'COVERED')
class PROCDET_V100(Aggregate): """OFX tax extensions section 2.2.11.2""" form8949code = String(1) dtaqd = DateTime() dtvar = Bool() dtsale = DateTime(required=True) secname = String(120) saledescription = String(120) numshrs = Decimal() costbasis = Decimal() salespr = Decimal(required=True) accruedmktdiscount = Decimal() longshort = OneOf("LONG", "SHORT") ordinary = Bool() washsale = Bool() fedtaxwh = Decimal() washsalelossdisallowed = Decimal() noncoveredsecurity = Bool() lossnotallowed = Bool() basisnotshown = Bool() form1099bnotreceived = Bool() collectible = Bool() statecode = String(2) stateidnum = String(32) statetaxwheld = Decimal() statecode2 = String(2) stateidnum2 = String(32) statetaxwheld2 = Decimal() fatca = Bool() requiredMutexes = [["dtaqd", "dtvar"]]
class SyncRqList(Aggregate): """Base class for *SYNCRQ""" token = String(10) tokenonly = Bool() refresh = Bool() rejectifmissing = Bool(required=True) requiredMutexes = [["token", "tokenonly", "refresh"]]
class INVACCTINFO(Aggregate): """ OFX section 13.6.2 """ invacctfrom = SubAggregate(INVACCTFROM, required=True) usproducttype = OneOf(*USPRODUCTTYPES, required=True) checking = Bool(required=True) svcstatus = OneOf(*SVCSTATUSES, required=True) invaccttype = OneOf(*INVACCTTYPES) optionlevel = String(40)
class STMTTRNRS(Aggregate): """ OFX section 11.4.2.2 """ trnuid = String(36, required=True) status = SubAggregate(STATUS, required=True) stmtrs = SubAggregate(STMTRS) @property def statement(self): return self.stmtrs
class WIRERQ(Aggregate): """ OFX section 11.9.1.1.1 """ bankacctfrom = SubAggregate(BANKACCTFROM, required=True) wirebeneficiary = SubAggregate(WIREBENEFICIARY, required=True) wiredestbank = SubAggregate(WIREDESTBANK) trnamt = Decimal(required=True) dtdue = DateTime() payinstruct = String(255)
class LOANINFO(Aggregate): """ OFX section 13.9.3 """ loanid = String(32, required=True) loandesc = String(32) initialloanbal = Decimal() loanstartdate = DateTime() currentloanbal = Decimal(required=True) dtasof = DateTime(required=True) loanrate = Decimal() loanpmtamt = Decimal() loanpmtfreq = OneOf(*LOANPMTFREQUENCIES) loanpmtsinitial = Integer(5) loanpmtsremaining = Integer(5) loanmaturitydate = DateTime() loantotalprojinterest = Decimal() loaninteresttodate = Decimal() loannextpmtdate = DateTime()