diff --git a/src/ofxstatement/ofx.py b/src/ofxstatement/ofx.py
index 888c7d5..8b034c6 100644
--- a/src/ofxstatement/ofx.py
+++ b/src/ofxstatement/ofx.py
@@ -155,6 +155,8 @@ def buildInvestTransactionList(self) -> None:
for security_id in dict.fromkeys(
map(lambda x: x.security_id, self.statement.invest_lines)
):
+ if security_id is None:
+ continue
tb.start("STOCKINFO", {})
tb.start("SECINFO", {})
tb.start("SECID", {})
@@ -199,12 +201,21 @@ def buildInvestTransactionList(self) -> None:
tb.end("INVSTMTMSGSRSV1")
def buildInvestTransaction(self, line: InvestStatementLine) -> None:
- # invest transactions must always have trntype and trntype_detailed
- if line.trntype is None or line.trntype_detailed is None:
+ # invest transactions must always have trntype
+ if line.trntype is None:
return
tb = self.tb
+ if line.trntype == "INVBANKTRAN":
+ tb.start(line.trntype, {})
+ bankTran = StatementLine(line.id, line.date, line.memo, line.amount)
+ bankTran.trntype = line.trntype_detailed
+ self.buildBankTransaction(bankTran)
+ self.buildText("SUBACCTFUND", "OTHER")
+ tb.end(line.trntype)
+ return
+
tran_type_detailed_tag_name = None
inner_tran_type_tag_name = None
if line.trntype.startswith("BUY"):
@@ -213,6 +224,10 @@ def buildInvestTransaction(self, line: InvestStatementLine) -> None:
elif line.trntype.startswith("SELL"):
tran_type_detailed_tag_name = "SELLTYPE"
inner_tran_type_tag_name = "INVSELL"
+ elif line.trntype == "TRANSFER":
+ # Transfer transactions don't have details or an envelope
+ tran_type_detailed_tag_name = None
+ inner_tran_type_tag_name = None
else:
tran_type_detailed_tag_name = "INCOMETYPE"
inner_tran_type_tag_name = (
@@ -220,7 +235,8 @@ def buildInvestTransaction(self, line: InvestStatementLine) -> None:
)
tb.start(line.trntype, {})
- self.buildText(tran_type_detailed_tag_name, line.trntype_detailed, False)
+ if tran_type_detailed_tag_name:
+ self.buildText(tran_type_detailed_tag_name, line.trntype_detailed, False)
if inner_tran_type_tag_name:
tb.start(inner_tran_type_tag_name, {})
@@ -266,12 +282,7 @@ def buildInvestTransaction(self, line: InvestStatementLine) -> None:
precision=self.invest_transactions_float_precision,
)
- self.buildAmount(
- "TOTAL",
- line.amount,
- False,
- precision=self.invest_transactions_float_precision,
- )
+ self.buildAmount("TOTAL", line.amount)
if inner_tran_type_tag_name:
tb.end(inner_tran_type_tag_name)
diff --git a/src/ofxstatement/statement.py b/src/ofxstatement/statement.py
index 8124621..859b8b7 100644
--- a/src/ofxstatement/statement.py
+++ b/src/ofxstatement/statement.py
@@ -32,9 +32,11 @@
INVEST_TRANSACTION_TYPES = [
"BUYSTOCK",
"BUYDEBT",
+ "INCOME",
+ "INVBANKTRAN",
"SELLSTOCK",
"SELLDEBT",
- "INCOME",
+ "TRANSFER",
]
INVEST_TRANSACTION_TYPES_DETAILED = [
@@ -44,6 +46,17 @@
"SELLSHORT", # open short sale
"DIV", # only for INCOME
"INTEREST", # only for INCOME
+ "CGLONG", # only for INCOME
+ "CGSHORT", # only for INCOME
+]
+
+INVBANKTRAN_TYPES_DETAILED = [
+ "INT",
+ "XFER",
+ "DEBIT",
+ "CREDIT",
+ "SRVCHG",
+ "OTHER",
]
ACCOUNT_TYPE = [
@@ -275,19 +288,39 @@ def assert_valid(self) -> None:
INVEST_TRANSACTION_TYPES,
)
- assert (
- self.trntype_detailed in INVEST_TRANSACTION_TYPES_DETAILED
- ), "trntype_detailed %s is not valid, must be one of %s" % (
- self.trntype_detailed,
- INVEST_TRANSACTION_TYPES_DETAILED,
- )
+ if self.trntype == "INVBANKTRAN":
+ assert self.trntype_detailed in INVBANKTRAN_TYPES_DETAILED, (
+ "trntype_detailed %s is not valid for INVBANKTRAN, must be one of %s"
+ % (
+ self.trntype_detailed,
+ INVBANKTRAN_TYPES_DETAILED,
+ )
+ )
+ elif self.trntype == "TRANSFER":
+ assert (
+ self.trntype_detailed is None
+ ), f"trntype_detailed '{self.trntype_detailed}' should be empty for TRANSFERS"
+ else:
+ assert (
+ self.trntype_detailed in INVEST_TRANSACTION_TYPES_DETAILED
+ ), "trntype_detailed %s is not valid, must be one of %s" % (
+ self.trntype_detailed,
+ INVEST_TRANSACTION_TYPES_DETAILED,
+ )
assert self.id
- assert self.security_id
- assert self.amount
-
- assert self.trntype == "INCOME" or self.units
- assert self.trntype == "INCOME" or self.unit_price
+ assert self.date
+ assert self.trntype == "TRANSFER" or self.amount
+ assert self.trntype == "INVBANKTRAN" or self.security_id
+
+ if self.trntype == "INVBANKTRAN":
+ pass
+ elif self.trntype == "INCOME":
+ assert self.security_id
+ else:
+ assert self.security_id
+ assert self.units
+ assert self.trntype == "TRANSFER" or self.unit_price
class BankAccount(Printable):
diff --git a/src/ofxstatement/tests/test_ofx_invest.py b/src/ofxstatement/tests/test_ofx_invest.py
index 433a5af..82ba15b 100644
--- a/src/ofxstatement/tests/test_ofx_invest.py
+++ b/src/ofxstatement/tests/test_ofx_invest.py
@@ -88,7 +88,7 @@
1.24000
138.28000
3.00000
- -416.08000
+ -416.08
@@ -108,7 +108,7 @@
0.28000
225.63000
-5.00000
- 1127.87000
+ 1127.87
@@ -125,8 +125,33 @@
OTHER
OTHER
0.50000
- 0.79000
+ 0.79
+
+
+ INT
+ 20210102
+ 0.45
+ 6
+ Bank Interest
+
+ OTHER
+
+
+
+ 7
+ 20210103
+ Journaled Shares
+
+
+ MSFT
+ TICKER
+
+ OTHER
+ OTHER
+ 225.63000
+ 4.00000
+
@@ -190,6 +215,22 @@ def test_ofxWriter(self) -> None:
invest_line.assert_valid()
statement.invest_lines.append(invest_line)
+ invest_line = InvestStatementLine(
+ "6", datetime(2021, 1, 2), "Bank Interest", "INVBANKTRAN", "INT"
+ )
+ invest_line.amount = Decimal("0.45")
+ invest_line.assert_valid()
+ statement.invest_lines.append(invest_line)
+
+ invest_line = InvestStatementLine(
+ "7", datetime(2021, 1, 3), "Journaled Shares", "TRANSFER"
+ )
+ invest_line.security_id = "MSFT"
+ invest_line.units = Decimal("4")
+ invest_line.unit_price = Decimal("225.63")
+ invest_line.assert_valid()
+ statement.invest_lines.append(invest_line)
+
# Create writer:
writer = ofx.OfxWriter(statement)
diff --git a/src/ofxstatement/tests/test_statement.py b/src/ofxstatement/tests/test_statement.py
index 730bc94..cb63320 100644
--- a/src/ofxstatement/tests/test_statement.py
+++ b/src/ofxstatement/tests/test_statement.py
@@ -50,3 +50,88 @@ def test_generate_unique_transaction_id(self) -> None:
self.assertTrue(tid2.endswith("-1"))
self.assertEqual(len(txnids), 2)
+
+ def test_transfer_line_validation(self) -> None:
+ line = statement.InvestStatementLine("id", datetime(2020, 3, 25))
+ line.trntype = "TRANSFER"
+ line.security_id = "ABC"
+ line.units = Decimal(2)
+ line.assert_valid()
+ with self.assertRaises(AssertionError):
+ line.security_id = None
+ line.assert_valid()
+ line.security_id = "ABC"
+ with self.assertRaises(AssertionError):
+ line.units = None
+ line.assert_valid()
+ line.units = Decimal(2)
+ with self.assertRaises(AssertionError):
+ line.trntype_detailed = "DETAIL"
+ line.assert_valid()
+
+ def test_invbank_line_validation(self) -> None:
+ line = statement.InvestStatementLine("id", datetime(2020, 3, 25))
+ line.trntype = "INVBANKTRAN"
+ line.trntype_detailed = "INT"
+ line.amount = Decimal(1)
+ line.assert_valid()
+ with self.assertRaises(AssertionError):
+ line.amount = None
+ line.assert_valid()
+ line.amount = Decimal(1)
+ with self.assertRaises(AssertionError):
+ line.trntype_detailed = "BLAH"
+ line.assert_valid()
+
+ def test_income_line_validation(self) -> None:
+ line = statement.InvestStatementLine("id", datetime(2020, 3, 25))
+ line.trntype = "INCOME"
+ line.trntype_detailed = "INTEREST"
+ line.amount = Decimal(1)
+ line.security_id = "AAPL"
+ line.assert_valid()
+ with self.assertRaises(AssertionError):
+ line.amount = None
+ line.assert_valid()
+ line.amount = Decimal(1)
+ with self.assertRaises(AssertionError):
+ line.trntype_detailed = "BLAH"
+ line.assert_valid()
+ line.trntype_detailed = "INTEREST"
+ with self.assertRaises(AssertionError):
+ line.security_id = None
+ line.assert_valid()
+
+ def test_buy_line_validation(self) -> None:
+ line = statement.InvestStatementLine("id", datetime(2020, 3, 25))
+ line.trntype = "BUYSTOCK"
+ line.trntype_detailed = "BUY"
+ line.amount = Decimal(1)
+ line.security_id = "AAPL"
+ line.units = Decimal(3)
+ line.unit_price = Decimal(1.1)
+ line.assert_valid()
+
+ with self.assertRaises(AssertionError):
+ line.amount = None
+ line.assert_valid()
+ line.amount = Decimal(1)
+
+ with self.assertRaises(AssertionError):
+ line.trntype_detailed = "BLAH"
+ line.assert_valid()
+ line.trntype_detailed = "INTEREST"
+
+ with self.assertRaises(AssertionError):
+ line.security_id = None
+ line.assert_valid()
+ line.security_id = "AAPL"
+
+ with self.assertRaises(AssertionError):
+ line.units = None
+ line.assert_valid()
+ line.units = Decimal(3)
+
+ with self.assertRaises(AssertionError):
+ line.unit_price = None
+ line.assert_valid()