def to_mint_transactions(self, t, skip_free_shipping=False): new_transactions = [] # More expensive items are always more interesting when it comes to # budgeting, so show those first (for both itemized and concatted). items = sorted( self.items, key=lambda item: item.item_total, reverse=True) # Itemize line-items: for i in items: new_cat = category.get_mint_category_from_unspsc(i.unspsc_code) item = t.split( amount=i.item_total, category=new_cat, desc=i.get_title(88), note=self.get_note()) new_transactions.append(item) # Itemize the shipping cost, if any. is_free_shipping = ( self.shipping_charge and self.total_promotions and micro_usd_nearly_equal( self.total_promotions, self.shipping_charge)) if is_free_shipping and skip_free_shipping: return new_transactions if self.shipping_charge: ship = t.split( amount=self.shipping_charge, category='Shipping', desc='Shipping', note=self.get_note()) new_transactions.append(ship) # All promotion(s) as one line-item. if self.total_promotions: # If there was a promo that matches the shipping cost, it's nearly # certainly a Free One-day/same-day/etc promo. In this case, # categorize the promo instead as 'Shipping', which will cancel out # in Mint trends. cat = ('Shipping' if is_free_shipping else category.DEFAULT_MINT_CATEGORY) promo = t.split( amount=-self.total_promotions, category=cat, desc='Promotion(s)', note=self.get_note(), is_debit=False) new_transactions.append(promo) return new_transactions
def test_(self): self.assertEqual(get_mint_category_from_unspsc(10110000), 'Pet Food & Supplies') self.assertEqual(get_mint_category_from_unspsc(10121806), 'Pet Food & Supplies') self.assertEqual(get_mint_category_from_unspsc(10111303), 'Pet Food & Supplies') self.assertEqual(get_mint_category_from_unspsc(14110000), 'Office Supplies') self.assertEqual(get_mint_category_from_unspsc(14111525), 'Office Supplies') self.assertEqual(get_mint_category_from_unspsc(14111700), 'Home Supplies') self.assertEqual(get_mint_category_from_unspsc(14111701), 'Home Supplies') self.assertEqual(get_mint_category_from_unspsc(14111703), 'Home Supplies') self.assertEqual(get_mint_category_from_unspsc(14111803), 'Office Supplies') self.assertEqual(get_mint_category_from_unspsc(25170000), 'Service & Parts') self.assertEqual(get_mint_category_from_unspsc(30181701), 'Home Improvement') self.assertEqual(get_mint_category_from_unspsc(39101600), 'Furnishings') self.assertEqual(get_mint_category_from_unspsc(40161504), 'Service & Parts') self.assertEqual(get_mint_category_from_unspsc(42142900), 'Personal Care') self.assertEqual(get_mint_category_from_unspsc(43211617), 'Electronics & Software') self.assertEqual(get_mint_category_from_unspsc(44121705), 'Office Supplies') self.assertEqual(get_mint_category_from_unspsc(47121701), 'Home Supplies') self.assertEqual(get_mint_category_from_unspsc(56101800), 'Baby Supplies') self.assertEqual(get_mint_category_from_unspsc(60141100), 'Toys') self.assertEqual(get_mint_category_from_unspsc(50000000), 'Groceries') self.assertEqual(get_mint_category_from_unspsc(50181900), 'Groceries') self.assertEqual(get_mint_category_from_unspsc(50192100), 'Groceries')
def get_mint_updates( orders, items, refunds, trans, args, stats, mint_category_name_to_id=category.DEFAULT_MINT_CATEGORIES_TO_IDS, progress_factory=no_progress_factory): mint_historic_category_renames = get_mint_category_history_for_items( trans, args) # Remove items from canceled orders. items = [i for i in items if not i.is_cancelled()] # Remove items that haven't shipped yet (also aren't charged). items = [i for i in items if i.order_status == 'Shipped'] # Remove items with zero quantity (it happens!) items = [i for i in items if i.quantity > 0] # Make more Items such that every item is quantity 1. This is critical # prior to associate_items_with_orders such that items with non-1 # quantities split into different packages can be associated with the # appropriate order. items = [si for i in items for si in i.split_by_quantity()] order_item_to_unspsc = dict( ((i.title, i.order_id), i.unspsc_code) for i in items) itemProgress = progress_factory( 'Matching Amazon Items with Orders', len(items)) amazon.associate_items_with_orders(orders, items, itemProgress) itemProgress.finish() # Only match orders that have items. orders = [o for o in orders if o.items] trans = mint.Transaction.unsplit(trans) stats['trans'] = len(trans) # Skip t if the original description doesn't contain 'amazon' merch_whitelist = args.mint_input_merchant_filter.lower().split(',') def get_original_names(t): """Returns a tuple of 'original' merchant strings to consider""" result = (t.omerchant.lower(), ) if args.mint_input_include_mmerchant: result = result + (t.mmerchant.lower(), ) if args.mint_input_include_merchant: result = result + (t.merchant.lower(), ) return result trans = [t for t in trans if any( any(merch_str in n for n in get_original_names(t)) for merch_str in merch_whitelist)] stats['amazon_in_desc'] = len(trans) # Skip t if it's pending. trans = [t for t in trans if not t.is_pending] stats['pending'] = stats['amazon_in_desc'] - len(trans) # Skip t if a category filter is given and t does not match. if args.mint_input_categories_filter: cat_whitelist = set( args.mint_input_categories_filter.lower().split(',')) trans = [t for t in trans if t.category.lower() in cat_whitelist] # Match orders. orderMatchProgress = progress_factory( 'Matching Amazon Orders w/ Mint Trans', len(orders)) match_transactions(trans, orders, args, orderMatchProgress) orderMatchProgress.finish() unmatched_trans = [t for t in trans if not t.orders] # Match refunds. refundMatchProgress = progress_factory( 'Matching Amazon Refunds w/ Mint Trans', len(refunds)) match_transactions(unmatched_trans, refunds, args, refundMatchProgress) refundMatchProgress.finish() unmatched_orders = [o for o in orders if not o.matched] unmatched_trans = [t for t in trans if not t.orders] unmatched_refunds = [r for r in refunds if not r.matched] num_gift_card = len([o for o in unmatched_orders if 'Gift Certificate' in o.payment_instrument_type]) num_unshipped = len([o for o in unmatched_orders if not o.shipment_date]) matched_orders = [o for o in orders if o.matched] matched_trans = [t for t in trans if t.orders] matched_refunds = [r for r in refunds if r.matched] stats['trans_unmatch'] = len(unmatched_trans) stats['order_unmatch'] = len(unmatched_orders) stats['refund_unmatch'] = len(unmatched_refunds) stats['trans_match'] = len(matched_trans) stats['order_match'] = len(matched_orders) stats['refund_match'] = len(matched_refunds) stats['skipped_orders_gift_card'] = num_gift_card stats['skipped_orders_unshipped'] = num_unshipped merged_orders = [] merged_refunds = [] updateCounter = progress_factory( 'Determining Mint Updates', len(matched_trans)) updates = [] for t in matched_trans: updateCounter.next() if t.is_debit: order = amazon.Order.merge(t.orders) merged_orders.extend(orders) prefix = '{}: '.format(order.website) if args.description_prefix_override: prefix = args.description_prefix_override if order.attribute_subtotal_diff_to_misc_charge(): stats['misc_charge'] += 1 # It's nice when "free" shipping cancels out with the shipping # promo, even though there is tax on said free shipping. Spread # that out across the items instead. # if order.attribute_itemized_diff_to_shipping_tax(): # stats['add_shipping_tax'] += 1 if order.attribute_itemized_diff_to_per_item_tax(): stats['adjust_itemized_tax'] += 1 assert micro_usd_nearly_equal(t.amount, order.total_charged) assert micro_usd_nearly_equal(t.amount, order.total_by_subtotals()) assert micro_usd_nearly_equal(t.amount, order.total_by_items()) new_transactions = order.to_mint_transactions( t, skip_free_shipping=not args.verbose_itemize) else: refunds = amazon.Refund.merge(t.orders) merged_refunds.extend(refunds) prefix = '{} refund: '.format(refunds[0].website) if args.description_return_prefix_override: prefix = args.description_return_prefix_override new_transactions = [] for r in refunds: new_tran = r.to_mint_transaction(t) new_transactions.append(new_tran) # Attempt to find the category from the original purchase. unspsc = order_item_to_unspsc.get((r.title, r.order_id), None) if unspsc: new_tran.category = category.get_mint_category_from_unspsc( unspsc) assert micro_usd_nearly_equal( t.amount, mint.Transaction.sum_amounts(new_transactions)) for nt in new_transactions: # Look if there's a personal category tagged. item_name = amazon.rm_leading_qty(nt.merchant.lower()) if (mint_historic_category_renames and item_name in mint_historic_category_renames): suggested_cat = mint_historic_category_renames[item_name] if suggested_cat != nt.category: stats['personal_cat'] += 1 nt.category = mint_historic_category_renames[item_name] nt.update_category_id(mint_category_name_to_id) summarize_single_item_order = ( t.is_debit and len(order.items) == 1 and not args.verbose_itemize) if args.no_itemize or summarize_single_item_order: new_transactions = mint.summarize_new_trans( t, new_transactions, prefix) else: new_transactions = mint.itemize_new_trans(new_transactions, prefix) if mint.Transaction.old_and_new_are_identical( t, new_transactions, ignore_category=args.no_tag_categories): stats['already_up_to_date'] += 1 continue valid_prefixes = ( args.amazon_domains.lower().split(',') + [prefix.lower()]) if any(t.merchant.lower().startswith(pre) for pre in valid_prefixes): if args.prompt_retag: if args.num_updates > 0 and len(updates) >= args.num_updates: break print('\nTransaction already tagged:') print_dry_run( [(t, new_transactions)], ignore_category=args.no_tag_categories) print('\nUpdate tag to proposed? [Yn] ') action = readchar.readchar() if action == '': exit(1) if action not in ('Y', 'y', '\r', '\n'): stats['user_skipped_retag'] += 1 continue stats['retag'] += 1 elif not args.retag_changed: stats['no_retag'] += 1 continue else: stats['retag'] += 1 else: stats['new_tag'] += 1 updates.append((t, new_transactions)) if args.num_updates > 0: updates = updates[:args.num_updates] return updates, unmatched_orders + unmatched_refunds