Esempio n. 1
0
    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')
Esempio n. 3
0
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