def test_average(self): # Identity, no aggregation. inv = I('40.50 JPY, 40.51 USD {1.01 CAD}, 40.52 CAD') self.assertEqual(inv.average(), inv) # Identity, no aggregation, with a mix of lots at cost and without cost. inv = I('40 USD {1.01 CAD}, 40 USD') self.assertEqual(inv.average(), inv) # Aggregation. inv = I('40 USD {1.01 CAD}, 40 USD {1.02 CAD}') self.assertEqual(inv.average(), I('80.00 USD {1.015 CAD}')) # Aggregation, more units. inv = I('2 HOOL {500 USD}, 3 HOOL {520 USD}, 4 HOOL {530 USD}') self.assertEqual(inv.average(), I('9 HOOL {520 USD}')) # Average on zero amount, same costs inv = I('2 HOOL {500 USD}') inv.add_amount(A('-2 HOOL'), Cost(D('500'), 'USD', None, None)) self.assertEqual(inv.average(), I('')) # Average on zero amount, different costs inv = I('2 HOOL {500 USD}') inv.add_amount(A('-2 HOOL'), Cost(D('500'), 'USD', datetime.date(2000, 1, 1), None)) self.assertEqual(inv.average(), I(''))
def test_aggregate_holdings_list_with_price(): holdings = [ Posting('Assets', A('10 HOOL'), None, None, None, None), Posting('Assets', A('14 TEST'), Cost(D('10'), 'HOOL', None, None), D('12'), None, None), ] assert aggregate_holdings_list(holdings) == \ Posting('Assets', Amount(D('24'), '*'), Cost(D('6.25'), 'HOOL', None, None), D('178') / 24, None, None)
def test_eq_and_sortkey__bycost(self): pos1 = Position(A("1 USD"), None) pos2 = Position(A("1 USD"), Cost(D('10'), 'USD', None, None)) pos3 = Position(A("1 USD"), Cost(D('11'), 'USD', None, None)) pos4 = Position(A("1 USD"), Cost(D('12'), 'USD', None, None)) positions = [pos3, pos2, pos1, pos4] self.assertEqual([pos1, pos2, pos3, pos4], sorted(positions)) for _ in range(64): random.shuffle(positions) self.assertEqual([pos1, pos2, pos3, pos4], sorted(positions))
def create_simple_posting_with_cost(entry, account, number, currency, cost_number, cost_currency): """Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. cost_number: A Decimal number or string to use for the posting's cost Amount. cost_currency: a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. """ if isinstance(account, str): pass if not isinstance(number, Decimal): number = D(number) if cost_number and not isinstance(cost_number, Decimal): cost_number = D(cost_number) units = Amount(number, currency) cost = Cost(cost_number, cost_currency, None, None) posting = Posting(account, units, cost, None, None, None) if entry is not None: entry.postings.append(posting) return posting
def test_currency_pair(self): self.assertEqual(("USD", None), Position(A('100 USD'), None).currency_pair()) self.assertEqual(("AAPL", "USD"), Position(A('100 AAPL'), Cost('43.23', 'USD', None, None)).currency_pair())
def convert_costspec_to_cost(posting): """Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Args: posting: An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. """ cost = posting.cost if isinstance(cost, position.CostSpec): if cost is not None: number_per = cost.number_per number_total = cost.number_total if number_total is not None: # Compute the per-unit cost if there is some total cost # component involved. units_number = abs(posting.units.number) cost_total = number_total if number_per is not MISSING: cost_total += number_per * units_number unit_cost = cost_total / units_number else: unit_cost = number_per new_cost = Cost(unit_cost, cost.currency, cost.date, cost.label) posting = posting._replace(units=posting.units, cost=new_cost) return posting
def test_from_amounts(self): pos = from_amounts(A('10.00 USD')) self.assertEqual(Position(A("10 USD")), pos) pos = from_amounts(A('10 HOOL'), A('510.00 USD')) self.assertEqual( Position(A("10 HOOL"), Cost(D('510'), 'USD', None, None)), pos)
def convert_spec_to_cost(units, cost_spec): """Convert a posting's CostSpec instance to a Cost. Args: units: An instance of Amount. cost_spec: An instance of CostSpec. Returns: An instance of Cost. """ cost = cost_spec errors = [] if isinstance(units, Amount): currency = units.currency if cost_spec is not None: number_per, number_total, cost_currency, date, label, merge = cost_spec # Compute the cost. if number_per is not MISSING or number_total is not None: if number_total is not None: # Compute the per-unit cost if there is some total cost # component involved. units_num = units.number cost_total = number_total if number_per is not MISSING: cost_total += number_per * units_num unit_cost = cost_total / abs(units_num) else: unit_cost = number_per cost = Cost(unit_cost, cost_currency, date, label) else: cost = None return cost
def test_units(self): units = A("100 HOOL") self.assertEqual( units, convert.get_units( self._pos(units, Cost(D("514.00"), "USD", None, None)))) self.assertEqual(units, convert.get_units(self._pos(units, None)))
def test_holdings_with_prices(load_doc): """ 2013-04-05 * Equity:Unknown Assets:Cash 50000 USD 2013-04-01 * Assets:Account1 15 HOOL {518.73 USD} Assets:Cash 2013-06-01 price HOOL 578.02 USD """ entries, _, _ = load_doc price_map = prices.build_price_map(entries) holdings = get_final_holdings(entries, ('Assets'), price_map) holdings = sorted(map(tuple, holdings)) assert holdings == [ ('Assets:Account1', A('15 HOOL'), Cost(D('518.73'), 'USD', datetime.date(2013, 4, 1), None), D('578.02'), None, None), ('Assets:Cash', A('42219.05 USD'), None, None, None, None), ]
def average(self): """Average all lots of the same currency together. Returns: An instance of Inventory. """ groups = collections.defaultdict(list) for position in self: key = (position.units.currency, position.cost.currency if position.cost else None) groups[key].append(position) average_inventory = Inventory() for (currency, cost_currency), positions in groups.items(): total_units = sum(position.units.number for position in positions) units_amount = Amount(total_units, currency) if cost_currency: total_cost = sum( convert.get_cost(position).number for position in positions) cost = Cost(total_cost / total_units, cost_currency, None, None) else: cost = None average_inventory.add_amount(units_amount, cost) return average_inventory
def test_quantities(self): pos = Position(A("10 USD"), None) self.assertEqual(A('10 USD'), pos.units) self.assertEqual(A('10 USD'), convert.get_cost(pos)) pos = Position(A("10 USD"), Cost(D('1.5'), 'AUD', None, None)) self.assertEqual(A('10 USD'), pos.units) self.assertEqual(A('15 AUD'), convert.get_cost(pos))
def test_from_string(self): inv = inventory.from_string('') self.assertEqual(Inventory(), inv) inv = inventory.from_string('10 USD') self.assertEqual( Inventory([Position(A("10 USD"))]), inv) inv = inventory.from_string(' 10.00 USD ') self.assertEqual( Inventory([Position(A("10 USD"))]), inv) inv = inventory.from_string('1 USD, 2 CAD') self.assertEqual( Inventory([Position(A("1 USD")), Position(A("2 CAD"))]), inv) inv = inventory.from_string('2.2 HOOL {532.43 USD}, 3.413 EUR') self.assertEqual( Inventory([Position(A("2.2 HOOL"), Cost(D('532.43'), 'USD', None, None)), Position(A("3.413 EUR"))]), inv) inv = inventory.from_string( '2.2 HOOL {532.43 USD}, 2.3 HOOL {564.00 USD, 2015-07-14}, 3.413 EUR') self.assertEqual( Inventory([Position(A("2.2 HOOL"), Cost(D('532.43'), 'USD', None, None)), Position(A("2.3 HOOL"), Cost(D('564.00'), 'USD', datetime.date(2015, 7, 14), None)), Position(A("3.413 EUR"))]), inv) inv = inventory.from_string( '1.1 HOOL {500.00 # 11.00 USD}, 100 CAD') self.assertEqual( Inventory([Position(A("1.1 HOOL"), Cost(D('510.00'), 'USD', None, None)), Position(A("100 CAD"))]), inv)
def test_add_amount__withlots(self): # Testing the strict case where everything matches, with only a cost. inv = Inventory() inv.add_amount(A('50 HOOL'), Cost(D('700'), 'USD', None, None)) self.checkAmount(inv, '50', 'HOOL') inv.add_amount(A('-40 HOOL'), Cost(D('700'), 'USD', None, None)) self.checkAmount(inv, '10', 'HOOL') position_, _ = inv.add_amount(A('-12 HOOL'), Cost(D('700'), 'USD', None, None)) self.assertTrue(next(iter(inv)).is_negative_at_cost()) # Testing the strict case where everything matches, a cost and a lot-date. inv = Inventory() inv.add_amount(A('50 HOOL'), Cost(D('700'), 'USD', date(2000, 1, 1), None)) self.checkAmount(inv, '50', 'HOOL') inv.add_amount(A('-40 HOOL'), Cost(D('700'), 'USD', date(2000, 1, 1), None)) self.checkAmount(inv, '10', 'HOOL') position_, _ = inv.add_amount( A('-12 HOOL'), Cost(D('700'), 'USD', date(2000, 1, 1), None)) self.assertTrue(next(iter(inv)).is_negative_at_cost())
def test_add_amount__allow_negative(self): inv = Inventory() # Test adding positions of different types. position_, _ = inv.add_amount(A('-11 USD')) self.assertIsNone(position_) position_, _ = inv.add_amount(A('-11 USD'), Cost(D('1.10'), 'CAD', None, None)) self.assertIsNone(position_) position_, _ = inv.add_amount(A('-11 USD'), Cost(D('1.10'), 'CAD', date(2012, 1, 1), None)) self.assertIsNone(position_) # Check for reductions. invlist = list(inv) self.assertTrue(invlist[1].is_negative_at_cost()) self.assertTrue(invlist[2].is_negative_at_cost()) inv.add_amount(A('-11 USD'), Cost(D('1.10'), 'CAD', None, None)) inv.add_amount(A('-11 USD'), Cost(D('1.10'), 'CAD', date(2012, 1, 1), None)) self.assertEqual(3, len(inv)) # Test adding to a position that does exist. inv = I('10 USD, 10 USD {1.10 CAD}, 10 USD {1.10 CAD, 2012-01-01}') position_, _ = inv.add_amount(A('-11 USD')) self.assertEqual(position_, position.from_string('10 USD')) position_, _ = inv.add_amount(A('-11 USD'), Cost(D('1.10'), 'CAD', None, None)) self.assertEqual(position_, position.from_string('10 USD {1.10 CAD}')) position_, _ = inv.add_amount(A('-11 USD'), Cost(D('1.10'), 'CAD', date(2012, 1, 1), None)) self.assertEqual(position_, position.from_string('10 USD {1.10 CAD, 2012-01-01}'))
def create_simple_posting_with_cost_or_price(entry, account, number, currency, price_number=None, price_currency=None, cost_number=None, cost_currency=None, costspec=None): """Create a simple posting on the entry, with a cost (for purchases) or price (for sell transactions). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. price_number: A Decimal number or string to use for the posting's price Amount. price_currency: a string, the currency for the price Amount. cost_number: A Decimal number or string to use for the posting's cost Amount. cost_currency: a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. """ if isinstance(account, str): pass if not isinstance(number, Decimal): number = D(number) units = Amount(number, currency) if not (price_number or cost_number): print("Either price ({}) or cost ({}) must be specified ({})".format( price_number, cost_number, entry)) import pdb pdb.set_trace() raise Exception( "Either price ({}) or cost ({}) must be specified".format( price_number, cost_number)) price = Amount(price_number, price_currency) if price_number else None cost = Cost(cost_number, cost_currency, None, None) if cost_number else None cost = costspec if costspec else cost posting = data.Posting(account, units, cost, price, None, None) if entry is not None: entry.postings.append(posting) return posting
def average(self): """Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. """ groups = collections.defaultdict(list) for position in self: key = (position.units.currency, position.cost.currency if position.cost else None) groups[key].append(position) average_inventory = Inventory() for (currency, cost_currency), positions in groups.items(): total_units = sum(position.units.number for position in positions) # Explicitly skip aggregates when resulting in zero units. if total_units == ZERO: continue units_amount = Amount(total_units, currency) if cost_currency: total_cost = sum(convert.get_cost(position).number for position in positions) cost_number = (Decimal('Infinity') if total_units == ZERO else (total_cost / total_units)) min_date = None for pos in positions: pos_date = pos.cost.date if pos.cost else None if pos_date is not None: min_date = (pos_date if min_date is None else min(min_date, pos_date)) cost = Cost(cost_number, cost_currency, min_date, None) else: cost = None average_inventory.add_amount(units_amount, cost) return average_inventory
def aggregate_holdings_list(holdings): if not holdings: return None units, total_book_value, total_market_value = ZERO, ZERO, ZERO accounts = set() currencies = set() cost_currencies = set() for pos in holdings: units += pos.units.number accounts.add(pos.account) currencies.add(pos.units.currency) cost_currencies.add( pos.cost.currency if pos.cost else pos.units.currency) if pos.cost: total_book_value += pos.units.number * pos.cost.number else: total_book_value += pos.units.number if pos.price is not None: total_market_value += pos.units.number * pos.price else: total_market_value += \ pos.units.number * (pos.cost.number if pos.cost else 1) assert len(cost_currencies) == 1 avg_cost = total_book_value / units if units else None avg_price = total_market_value / units if units else None currency = currencies.pop() if len(currencies) == 1 else '*' cost_currency = cost_currencies.pop() account_ = (accounts.pop() if len(accounts) == 1 else account.commonprefix(accounts)) show_cost = bool(avg_cost) and cost_currency != currency return Posting( account_, Amount(units, currency), Cost(avg_cost, cost_currency, None, None) if show_cost else None, avg_price if show_cost else None, None, None)
'amount': '' }, ], } with app.test_request_context(): serialised = loads(dumps(serialise(txn))) assert serialised == json_txn @pytest.mark.parametrize( 'pos,amount', [ ((A('100 USD'), None, None, None, None), '100 USD'), ( (A('100 USD'), Cost(D('10'), 'EUR', None, None), None, None, None), '100 USD {10 EUR}', ), ( ( A('100 USD'), Cost(D('10'), 'EUR', None, None), A('11 EUR'), None, None, ), '100 USD {10 EUR} @ 11 EUR', ), ((A('100 USD'), None, A('11 EUR'), None, None), '100 USD @ 11 EUR'), ( (
def test_from_string__with_label(self): pos = from_string('2.2 HOOL {"78c3f7f1315b"}') self.assertEqual( Position(A("2.2 HOOL"), Cost(None, None, None, "78c3f7f1315b")), pos)
def test_from_string__with_compound_cost(self): pos = from_string('1.1 HOOL {500.00 # 11.00 USD}') self.assertEqual( Position(A("1.1 HOOL"), Cost(D('510.00'), 'USD', None, None)), pos)
def test_from_string__with_everything(self): pos = from_string( '20 HOOL {532.43 # 20.00 USD, "e4dc1a361022", 2014-06-15}') cost = Cost(D('533.43'), 'USD', datetime.date(2014, 6, 15), "e4dc1a361022") self.assertEqual(Position(A("20 HOOL"), cost), pos)
def test_constructors(self): Position(A('123.45 USD'), None) Position(A('123.45 USD'), Cost('74.00', 'CAD', None, None)) Position(A('123.45 USD'), Cost('74.00', 'CAD', date(2013, 2, 3), None)) Position(Amount(D('123.45'), None), None) Position(Amount(None, 'USD'), None)
def test_negative(self): pos = Position(A("28372 USD"), Cost(D('10'), 'AUD', None, None)) negpos = pos.get_negative() self.assertEqual(A('-28372 USD'), negpos.units) self.assertEqual(A('-283720 AUD'), convert.get_cost(negpos))
def test_get_final_holdings(load_doc): """ 2013-04-05 * Equity:Unknown Assets:Cash 50000 USD 2013-04-01 * Assets:Account1 15 HOOL {518.73 USD} Assets:Cash 2013-04-02 * Assets:Account1 10 HOOL {523.46 USD} Assets:Cash 2013-04-03 * Assets:Account1 -4 HOOL {518.73 USD} Assets:Cash 2013-04-02 * Assets:Account2 20 ITOT {85.195 USD} Assets:Cash 2013-04-03 * Assets:Account3 50 HOOL {540.00 USD} @ 560.00 USD Assets:Cash 2013-04-10 * Assets:Cash 5111 USD Liabilities:Loan """ entries, _, _ = load_doc holdings = get_final_holdings(entries) holdings = sorted(map(tuple, holdings)) assert holdings == [ ('Assets:Account1', A('10 HOOL'), Cost(D('523.46'), 'USD', None, None), None, None, None), ('Assets:Account1', A('11 HOOL'), Cost(D('518.73'), 'USD', None, None), None, None, None), ('Assets:Account2', A('20 ITOT'), Cost(D('85.195'), 'USD', None, None), None, None, None), ('Assets:Account3', A('50 HOOL'), Cost(D('540.00'), 'USD', None, None), None, None, None), ('Assets:Cash', A('15466.470 USD'), None, None, None, None), ('Equity:Unknown', A('-50000 USD'), None, None, None, None), ('Liabilities:Loan', A('-5111 USD'), None, None, None, None), ] holdings = get_final_holdings(entries, ('Assets')) holdings = sorted(map(tuple, holdings)) assert holdings == [ ('Assets:Account1', A('10 HOOL'), Cost(D('523.46'), 'USD', None, None), None, None, None), ('Assets:Account1', A('11 HOOL'), Cost(D('518.73'), 'USD', None, None), None, None, None), ('Assets:Account2', A('20 ITOT'), Cost(D('85.195'), 'USD', None, None), None, None, None), ('Assets:Account3', A('50 HOOL'), Cost(D('540.00'), 'USD', None, None), None, None, None), ('Assets:Cash', A('15466.470 USD'), None, None, None, None), ]
def test_is_negative_at_cost(self): pos1 = Position(A("1 USD"), Cost('10', 'AUD', None, None)) pos2 = Position(A("-1 USD"), Cost('10', 'AUD', None, None)) self.assertFalse(pos1.is_negative_at_cost()) self.assertTrue(pos2.is_negative_at_cost())
def oneliner(entries, options_map, config): """Parse note oneliners into valid transactions. For example, 1999-12-31 note Assets:Cash "Income:Test -16.18 EUR * Description goes here *" """ errors = [] new_entries = [] for entry in entries: if(isinstance(entry, data.Note) and entry.comment[-1:] == "*"): comment = entry.comment try: k = None maybe_cost = RE_COST.findall(comment) if len(maybe_cost) > 0: amount = maybe_cost[0].split()[0] currency = maybe_cost[0].split()[1] cost = Cost(D(amount), currency, None, None) k = mul(cost, D(-1)) comment = RE_COST.sub('', comment) else: cost = None maybe_price = RE_PRICE.findall(comment) if len(maybe_price) > 0: price = Amount.from_string(maybe_price[0]) k = k or mul(price, D(-1)) comment = RE_PRICE.sub('', comment) else: price = None comment_tuple = comment.split() other_account = comment_tuple[0] units = Amount.from_string(' '.join(comment_tuple[1:3])) flag = comment_tuple[3] narration_tmp = ' '.join(comment_tuple[4:-1]) tags = {'NoteToTx'} for tag in RE_TAG.findall(narration_tmp): tags.add( tag[1] ) narration = RE_TAG.sub('', narration_tmp).rstrip() k = k or Amount(D(-1), units.currency) # print(type(cost), cost, type(price), price, type(units), units, k, comment) p1 = data.Posting(account=other_account, units=units, cost=cost, price=price, flag=None, meta={'filename': entry.meta['filename'], 'lineno': entry.meta['lineno']}) p2 = data.Posting(account=entry.account, units=mul(k, units.number), cost=cost, price=None, flag=None, meta={'filename': entry.meta['filename'], 'lineno': entry.meta['lineno']}) e = data.Transaction(date=entry.date, flag=flag, payee=None, # TODO narration=narration, tags=tags, # TODO links=EMPTY_SET, # TODO postings=[p1, p2], meta=entry.meta) new_entries.append(e) # print(e) except: print('beancount_oneliner error:', entry, sys.exc_info()) else: new_entries.append(entry) return new_entries, errors
def booking_method_AVERAGE(entry, posting, matches): """AVERAGE booking method implementation.""" booked_reductions = [] booked_matches = [] errors = [AmbiguousMatchError(entry.meta, "AVERAGE method is not supported", entry)] return booked_reductions, booked_matches, errors, False # FIXME: Future implementation here. # pylint: disable=unreachable if False: # pylint: disable=using-constant-test # DISABLED - This is the code for AVERAGE, which is currently disabled. # If there is more than a single match we need to ultimately merge the # postings. Also, if the reducing posting provides a specific cost, we # need to update the cost basis as well. Both of these cases are carried # out by removing all the matches and readding them later on. if len(matches) == 1 and ( not isinstance(posting.cost.number_per, Decimal) and not isinstance(posting.cost.number_total, Decimal)): # There is no cost. Just reduce the one leg. This should be the # normal case if we always merge augmentations and the user lets # Beancount deal with the cost. match = matches[0] sign = -1 if posting.units.number < ZERO else 1 number = min(abs(match.units.number), abs(posting.units.number)) match_units = Amount(number * sign, match.units.currency) booked_reductions.append(posting._replace(units=match_units, cost=match.cost)) insufficient = (match_units.number != posting.units.number) else: # Merge the matching postings to a single one. merged_units = inventory.Inventory() merged_cost = inventory.Inventory() for match in matches: merged_units.add_amount(match.units) merged_cost.add_amount(convert.get_weight(match)) if len(merged_units) != 1 or len(merged_cost) != 1: errors.append( AmbiguousMatchError( entry.meta, 'Cannot merge positions in multiple currencies: {}'.format( ', '.join(position.to_string(match_posting) for match_posting in matches)), entry)) else: if (isinstance(posting.cost.number_per, Decimal) or isinstance(posting.cost.number_total, Decimal)): errors.append( AmbiguousMatchError( entry.meta, "Explicit cost reductions aren't supported yet: {}".format( position.to_string(posting)), entry)) else: # Insert postings to remove all the matches. booked_reductions.extend( posting._replace(units=-match.units, cost=match.cost, flag=flags.FLAG_MERGING) for match in matches) units = merged_units[0].units date = matches[0].cost.date ## FIXME: Select which one, ## oldest or latest. cost_units = merged_cost[0].units cost = Cost(cost_units.number/units.number, cost_units.currency, date, None) # Insert a posting to refill those with a replacement match. booked_reductions.append( posting._replace(units=units, cost=cost, flag=flags.FLAG_MERGING)) # Now, match the reducing request against this lot. booked_reductions.append( posting._replace(units=posting.units, cost=cost)) insufficient = abs(posting.units.number) > abs(units.number)
def _make_import_result(self, x) -> ImportResult: if x._fields == DepositShares._fields: deposit = DepositShares(*x) cash_basis = Amount(number=round_to( deposit.vest_fmv.number * deposit.shares, D("0.001")), currency=deposit.vest_fmv.currency) transaction = Transaction( meta=None, date=deposit.date, flag=FLAG_OKAY, payee=None, narration='Deposit %s as %d %s shares' % (cash_basis, deposit.shares, deposit.symbol), tags=EMPTY_SET, links={deposit.award_id}, postings=[ Posting(account=self.eac_account, units=Amount(deposit.shares, deposit.symbol), cost=Cost(number=deposit.vest_fmv.number, currency=deposit.vest_fmv.currency, date=deposit.vest_date, label=None), price=None, flag=None, meta=collections.OrderedDict( source_desc=deposit.source_desc, date=deposit.date, award_date=deposit.award_date, )), Posting( account=self.stock_income_account, units=-cash_basis, cost=None, price=None, flag=None, meta=None, ), ]) elif x._fields == JournalTransfer._fields: tran = JournalTransfer(*x) dest_account = FIXME_ACCOUNT narration = tran.description if tran.symbol in self.commodity_dest_acct: dest_account = self.commodity_dest_acct[tran.symbol] if len(tran.basis) > 0: postings = [] set_meta = False narration = f"{tran.shares} {tran.symbol} {narration}" for basis in tran.basis: units = Amount(basis.shares, tran.symbol) meta = collections.OrderedDict() if not set_meta: meta = collections.OrderedDict( source_desc=tran.source_desc, date=tran.date, ) set_meta = True postings.append( Posting(account=self.eac_account, units=-units, cost=Cost(basis.vest_fmv.number, basis.vest_fmv.currency, basis.vest_date, None), price=None, flag=None, meta=meta)) postings.append( Posting( account=dest_account, units=units, cost=Cost(basis.vest_fmv.number, basis.vest_fmv.currency, basis.vest_date, None), price=None, flag=None, meta=None, )) else: postings = [ Posting(account=self.eac_account, units=tran.cash, cost=None, price=None, flag=None, meta=collections.OrderedDict( source_desc=tran.source_desc, date=tran.date, )), Posting( account=dest_account, units=-tran.cash, cost=None, price=None, flag=None, meta=None, ) ] if tran.action == "Journal": # TODO: infer correct account from description dest_account = self.cash_account transaction = Transaction(meta=None, date=tran.date, flag=FLAG_OKAY, payee=None, narration=narration, tags=EMPTY_SET, links=EMPTY_SET, postings=postings) elif x._fields == Sale._fields: sale = Sale(*x) transaction = Transaction( meta=None, date=sale.date, flag=FLAG_OKAY, payee=None, narration=sale.description + " %s %s" % (sale.shares, sale.symbol), tags=EMPTY_SET, links=set(map(lambda b: b.grant_id, sale.basis)), postings=[ Posting(account=self.eac_account, units=sale.cash, cost=None, price=None, flag=None, meta=collections.OrderedDict( source_desc=sale.source_desc, date=sale.date, )), Posting( account=self.fees_account, units=sale.fees, cost=None, price=None, flag=None, meta=None, ), ]) for basis in sale.basis: isLong = sale.date - basis.vest_date > datetime.timedelta(365) costbasis = basis.vest_fmv.number * basis.shares gain = basis.gross_proceeds.number - costbasis transaction.postings.append( Posting( account=self.eac_account, units=Amount(-basis.shares, sale.symbol), cost=Cost( number=basis.vest_fmv.number, currency=basis.vest_fmv.currency, date=basis.vest_date, label=None, ), price=basis.sale_price, flag=None, meta=collections.OrderedDict(basis=costbasis, ), )) # breaking gain out per basis makes it easier to file on taxes pnl_suffix = ":Long" if isLong else ":Short" transaction.postings.append( Posting( account=self.pnl_account + pnl_suffix, units=Amount(-gain, "USD"), cost=None, price=None, flag=None, meta=None, )) else: raise RuntimeError("Invalid import: " + x) return ImportResult(date=x.date, info=dict(type='text/csv', filename=x.file, line=1), entries=[transaction])
txn = txn._replace(payee="") json_txn["payee"] = "" serialised = loads(dumps(serialise(txn))) assert serialised == json_txn txn = txn._replace(payee=None) serialised = loads(dumps(serialise(txn))) assert serialised == json_txn @pytest.mark.parametrize( "pos,amount", [ ((A("100 USD"), None, None, None, None), "100 USD"), ( (A("100 USD"), Cost(D("10"), "EUR", None, None), None, None, None), "100 USD {10 EUR}", ), ( ( A("100 USD"), Cost(D("10"), "EUR", None, None), A("11 EUR"), None, None, ), "100 USD {10 EUR} @ 11 EUR", ), ((A("100 USD"), None, A("11 EUR"), None, None), "100 USD @ 11 EUR"), ( (