#
#   Copyright (c) 2014-2015, 2018, 2021 eGauge Systems LLC
# 	1644 Conestoga St, Suite 2
# 	Boulder, CO 80301
# 	voice: 720-545-9767
# 	email: davidm@egauge.net
#
#   All rights reserved.
#
#   This code is the property of eGauge Systems LLC and may not be
#   copied, modified, or disclosed without any prior and written
#   permission from eGauge Systems LLC.
#
# pylint: disable=invalid-name
'''Unit tests.  Run, e.g., from epic-sample with:

	python3 manage.py test epic.tests
'''
import io
import os
import re

from datetime import timedelta, datetime
from decimal import Decimal

from django.test import TestCase
from django.utils import timezone
from django.contrib.auth import get_user_model
from django.contrib.auth import models as auth_models

import epic
from epic import perms
from epic.base import Enum
from epic.compat import reverse
from epic.lib import get_stock
from epic.models import Assembly_Item, Delta, Inventory, Line_Item, Order, \
    Part, Shipment, Vendor, Vendor_Part, Warehouse
from epic import urls

def mktime(time_string):
    dt = datetime.strptime(time_string, '%b %d %Y %I:%M%p')
    return timezone.make_aware(dt, timezone.utc)

class URL_Iterator:
    def __init__(self, urlpatterns):
        self.urlpattern_iterator = urlpatterns.__iter__()

    def __iter__(self):
        return self

    def __next__(self):
        # pylint: disable=protected-access
        try:
            pat = self.urlpattern_iterator.__next__() # Python 3
        except AttributeError:
            pat = self.urlpattern_iterator.next() # Python 2
        regex = re.compile(pat.pattern._regex)
        args = {}
        for key in regex.groupindex.keys():
            args[key] = 1
        url = reverse('epic:%s' % pat.name, kwargs=args)
        return url, pat.name

    def next(self):
        """For Python 2 compatibility."""
        return self.__next__()

def create_part(pk):
    return Part(pk=pk, mfg='mfg', mfg_pn='mfg_pn%d' % pk,
                mounting=Enum.MOUNTING_SMD, target_price=1, overage=0,
                spq=1, status=Enum.STATUS_ACTIVE, lead_time=9)

def create_warehouse(pk, name):
    return Warehouse(pk=pk, name=name)

def create_vendor(pk, name):
    return Vendor(pk=pk, name=name)

def create_vendor_part(part_id, vendor_id, vendor_pn, price):
    return Vendor_Part(part_id=part_id, vendor_id=vendor_id,
                       vendor_pn=vendor_pn, price=price)

def create_order(pk, vendor_id, warehouse_id):
    now = timezone.now()
    then = now + timedelta(days=30)
    return Order(pk=pk, ts=now, warehouse_id=warehouse_id,
                 expected_arrival_date=then,
                 status=Enum.TXTN_STATUS_OPEN, vendor_id=vendor_id)

def create_ship(pk, order_id, warehouse_id, tracking='1Z9AA10123456784',
                cost_freight=0, cost_other=0, cost_discount=0):
    return Shipment(pk=pk, ts=timezone.now(), tracking=tracking,
                    ordr_id=order_id, warehouse_id=warehouse_id,
                    cost_freight=cost_freight, cost_other=cost_other,
                    cost_discount=cost_discount)

def create_inventory(pk, warehouse_id):
    return Inventory(pk=pk, ts=timezone.now(), warehouse_id=warehouse_id)

def create_assembly_item(assy_id=None, comp_id=None, qty=1):
    return Assembly_Item(assy_id=assy_id, comp_id=comp_id, qty=qty)

def create_line_item(txtn_id, part_id, qty, line_cost, index):
    return Line_Item(txtn_id=txtn_id, part_id=part_id, qty=qty,
                     line_cost=line_cost, index=index)


class Stock_Cache_Tests(TestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        cls.p = []	# parts
        cls.w = []	# warehouses
        cls.v = []	# vendors

        for i in range(10):
            p = create_part(i)
            p.save()
            cls.p.append(p)

        for i in range(3):
            w = create_warehouse(i, 'Warehouse%d' % i)
            w.save()
            cls.w.append(w)

        for i in range(5):
            v = create_vendor(i, 'Vendor%d' % i)
            v.save()
            cls.v.append(v)

    def assertInventory(self, inv, expected):
        for w in inv.warehouse:
            if w not in expected:
                self.fail('Warehouse %d unexpectedly in inventory.' % w)
            else:
                for p in inv.warehouse[w]:
                    if p not in expected[w]:
                        self.fail('Warehouse %d: part %d unexpectedly in '
                                  'inventory.' % (w, p))
                    else:
                        if inv.warehouse[w][p].qty != expected[w][p]:
                            self.fail('Warehouse %d, part %d: wrong qty %d; '
                                      'expected %d!' \
                                      % (w, p, inv.warehouse[w][p].qty,
                                         expected[w][p]))

                for p in expected[w]:
                    if p not in inv.warehouse[w]:
                        self.fail('Warehouse %d: part %d missing from '
                                  'inventory.' % (w, p))

        for w in expected:
            if w not in inv.warehouse:
                self.fail('Warehouse %d missing from inventory.' % w)

    def test_basic(self):
        inv0 = create_inventory(0, 0)
        inv0.ts = mktime('Jan 1 2010 4:00pm')
        inv0.save()
        inv0.finalize()

        d = Delta(part=self.__class__.p[0], is_absolute=True, adj=10, txtn=inv0,
                  warehouse=self.__class__.w[0])
        d.save()
        d = Delta(part=self.__class__.p[5], is_absolute=True, adj=20, txtn=inv0,
                  warehouse=self.__class__.w[0])
        d.save()
        d = Delta(part=self.__class__.p[3], is_absolute=True, adj=1000,
                  txtn=inv0, warehouse=self.__class__.w[2])
        d.save()

        inv = get_stock()
        self.assertInventory(inv, {0: {0: 10, 5: 20}, 2: {3: 1000}})

        inv = get_stock(warehouses=[0])
        self.assertInventory(inv, {0: {0: 10, 5: 20}})

        inv = get_stock(warehouses=[1])
        self.assertInventory(inv, {})

        inv = get_stock(warehouses=[2])
        self.assertInventory(inv, {2: {3: 1000}})

        inv = get_stock(exclude_txtn_id=inv0.id)
        self.assertInventory(inv, {})

        inv = get_stock(as_of_ts=mktime('Jan 1 2010 3:59pm'))
        self.assertInventory(inv, {})
        inv = get_stock(as_of_date=mktime('Jan 1 2010 5:00pm'))
        self.assertInventory(inv, {})

    def test_shipment_timestamp_change(self):
        '''Test shipment that moves between timestamps.'''
        o = create_order(pk=0, vendor_id=0, warehouse_id=0)
        o.save()
        o.finalize()
        s = create_ship(pk=1, order_id=o.id, warehouse_id=0)
        s.ts = mktime('Jan 1 2010 4:00pm')
        s.save()
        l = create_line_item(txtn_id=s.id, part_id=1, qty=2, line_cost='2',
                             index=1)
        l.save()
        s.finalize()

        inv = get_stock(as_of_date=mktime('Jan 1 2015 5:00pm'))
        self.assertInventory(inv, {0: {1: 2}})
        inv = get_stock(as_of_date=mktime('Jan 1 2001 5:00pm'))
        self.assertInventory(inv, {})

        s.ts = mktime('Jan 1 2017 5:00pm')
        s.save()
        s.finalize()

        inv = get_stock(as_of_date=mktime('Jan 1 2015 5:00pm'))
        self.assertInventory(inv, {})
        inv = get_stock(as_of_date=mktime('Jan 1 2001 5:00pm'))
        self.assertInventory(inv, {})

        s.ts = mktime('Jan 1 2010 4:00pm')
        s.save()
        s.finalize()

        inv = get_stock(as_of_date=mktime('Jan 1 2015 5:00pm'))
        self.assertInventory(inv, {0: {1: 2}})
        inv = get_stock(as_of_date=mktime('Jan 1 2001 5:00pm'))
        self.assertInventory(inv, {})

    def test_inventory_timestamp_change(self):
        '''Test inventory that moves between timestamps.'''
        inv0 = create_inventory(0, 0)
        inv0.ts = mktime('Jan 1 2010 4:00pm')
        inv0.save()
        inv0.finalize()

        d = Delta(part=self.__class__.p[0], is_absolute=True, adj=10, txtn=inv0,
                  warehouse=self.__class__.w[0])
        d.save()
        d = Delta(part=self.__class__.p[5], is_absolute=True, adj=20, txtn=inv0,
                  warehouse=self.__class__.w[0])
        d.save()
        d = Delta(part=self.__class__.p[3], is_absolute=True, adj=1000,
                  txtn=inv0, warehouse=self.__class__.w[2])
        d.save()

        inv = get_stock(as_of_date=mktime('Jan 1 2015 5:00pm'))
        self.assertInventory(inv, {0: {0: 10, 5: 20}, 2: {3: 1000}})
        inv = get_stock(as_of_date=mktime('Jan 1 2001 5:00pm'))
        self.assertInventory(inv, {})

        inv0.ts = mktime('Jan 1 2017 5:00pm')
        inv0.save()
        inv0.finalize()

        inv = get_stock(as_of_date=mktime('Jan 1 2015 5:00pm'))
        self.assertInventory(inv, {})
        inv = get_stock(as_of_date=mktime('Jan 1 2001 5:00pm'))
        self.assertInventory(inv, {})

        inv0.ts = mktime('Jan 1 2010 4:00pm')
        inv0.save()
        inv0.finalize()

        inv = get_stock(as_of_date=mktime('Jan 1 2015 5:00pm'))
        self.assertInventory(inv, {0: {0: 10, 5: 20}, 2: {3: 1000}})
        inv = get_stock(as_of_date=mktime('Jan 1 2001 5:00pm'))
        self.assertInventory(inv, {})

    def test_warehouse_delete(self):
        '''Test warehouse delete effect on stock cache.'''
        item = create_assembly_item(assy_id=3, comp_id=1, qty=10)
        item.save()

        inv0 = create_inventory(pk=0, warehouse_id=2)
        inv0.ts = mktime('Jan 1 2010 4:00pm')
        inv0.save()
        inv0.finalize()

        d = Delta(part=self.__class__.p[0], is_absolute=True, adj=10, txtn=inv0,
                  warehouse=self.__class__.w[0])
        d.save()
        d = Delta(part=self.__class__.p[5], is_absolute=True, adj=20, txtn=inv0,
                  warehouse=self.__class__.w[0])
        d.save()
        d = Delta(part=self.__class__.p[3], is_absolute=True, adj=1000,
                  txtn=inv0, warehouse=self.__class__.w[2])
        d.save()

        inv = get_stock()
        self.assertInventory(inv, {0: {0: 10, 5: 20}, 2: {3: 1000}})

        Warehouse.objects.filter(pk=0).delete()

        inv = get_stock()
        self.assertInventory(inv, {2: {3: 1000}})

class Part_Method_Tests(TestCase):

    def test_is_orderable(self):
        p = Part(status=Enum.STATUS_PREVIEW)
        self.assertEqual(p.is_orderable(), False)

        p = Part(status=Enum.STATUS_ACTIVE)
        self.assertEqual(p.is_orderable(), True)

        p = Part(status=Enum.STATUS_DEPRECATED)
        self.assertEqual(p.is_orderable(), True)

        p = Part(status=Enum.STATUS_OBSOLETE)
        self.assertEqual(p.is_orderable(), False)

    def test_equivalent_parts(self):
        p1 = create_part(1)
        p1.save()
        p2 = create_part(2)
        p2.save()

        self.assertEqual(p1.equivalent_parts(), [p1])

        p1.substitute = p2
        p1.save()
        p2.save()
        self.assertEqual(p1.equivalent_parts(), [p1, p2])
        self.assertEqual(p2.equivalent_parts(), [p1, p2])

        p3 = create_part(3)
        p3.substitute = p2
        p3.save()
        self.assertEqual(p1.equivalent_parts(), [p1, p2, p3])
        self.assertEqual(p2.equivalent_parts(), [p1, p2, p3])
        self.assertEqual(p3.equivalent_parts(), [p1, p2, p3])

        p1.substitute = None
        p1.save()
        self.assertEqual(p1.equivalent_parts(), [p1])
        self.assertEqual(p2.equivalent_parts(), [p2, p3])
        self.assertEqual(p3.equivalent_parts(), [p2, p3])

        p1.delete()
        p2.delete()
        p3.delete()

    def test_best_parts(self):
        p1 = create_part(1)
        p1.status = Enum.STATUS_PREFERRED
        p1.save()

        p2 = create_part(2)
        p2.status = Enum.STATUS_PREFERRED
        p2.substitute = p1
        p2.save()

        # p1 has lower ID so it should be preferred:
        self.assertEqual(p1.best_part(), p1)
        self.assertEqual(p2.best_part(), p1)

        p2.target_price = p1.target_price - 1
        p2.save()

        # p2 has lower price so it should be preferred:
        self.assertEqual(p1.best_part(), p2)
        self.assertEqual(p2.best_part(), p2)

        p2.status = Enum.STATUS_ACTIVE
        p2.save()
        # p1 is preferred, even though it's higher price
        self.assertEqual(p1.best_part(), p1)
        self.assertEqual(p2.best_part(), p1)

        p1.status = Enum.STATUS_ACTIVE
        p1.save()
        # now p2 should win again due to lower price:
        self.assertEqual(p1.best_part(), p2)
        self.assertEqual(p2.best_part(), p2)

    def test_avg_cost(self):
        part = []
        for i in range(3):
            p = create_part(i)
            if i == 1:
                p.substitute = part[0]
            p.save()
            part.append(p)

        for i in range(3):
            w = create_warehouse(i, 'Warehouse%d' % i)
            w.save()

        for i in range(5):
            v = create_vendor(i, 'Vendor%d' % i)
            v.save()

        # create shipment with:
        #	part 1 cost average = $1
        #	part 2 cost average = $0.001
        o = create_order(pk=0, vendor_id=0, warehouse_id=0)
        o.save()
        o.finalize()
        s = create_ship(pk=1, order_id=o.id, warehouse_id=0)
        s.save()
        l = create_line_item(txtn_id=s.id, part_id=0, qty=1, line_cost='3',
                             index=1)
        l.save()
        l = create_line_item(txtn_id=s.id, part_id=1, qty=1, line_cost='1',
                             index=2)
        l.save()
        l = create_line_item(txtn_id=s.id, part_id=2, qty=1000, line_cost='1',
                             index=3)
        l.save()
        s.finalize()

        # Create shipment over a year ago.  It should not affect average prices
        o = create_order(pk=2, vendor_id=0, warehouse_id=0)
        o.save()
        o.finalize()
        s = create_ship(pk=3, order_id=o.id, warehouse_id=0)
        s.ts = timezone.now() - timedelta(days=365)
        s.save()
        l = create_line_item(txtn_id=s.id, part_id=1, qty=1, line_cost='1000',
                             index=1)
        l.save()
        l = create_line_item(txtn_id=s.id, part_id=2, qty=1, line_cost='1000',
                             index=2)
        l.save()
        s.finalize()

        p = Part.objects.get(pk=0)
        self.assertEqual(p.avg_cost(), Decimal('2.000'))

        p = Part.objects.get(pk=1)
        self.assertEqual(p.avg_cost(), Decimal('2.000'))

        p = Part.objects.get(pk=2)
        self.assertEqual(p.avg_cost(), Decimal('0.001'))

class Access_Tests(TestCase):
    @classmethod
    def setup_part(cls):
        p1 = create_part(1)
        p1.save()
        ai = create_assembly_item(assy_id=1, comp_id=2, qty=3)
        ai.refdes = 'R1,R2,R3'
        ai.save()
        p = create_vendor_part(1, 2, 'ASSY_HOUSE_VENDOR_PART_#1', 325.654321)
        p.save()

    @classmethod
    def setup_vendor(cls):
        v = create_vendor(1, 'Vendor #1')
        v.save()
        p = create_vendor_part(1, 1, 'VENDOR_PART_#1', 535.123456)
        p.save()

    @classmethod
    def setup_warehouse(cls):
        w = create_warehouse(1, 'Warehouse #1')
        w.save()

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        perms.add_custom_permissions(auth_models)
        u = get_user_model().objects.create_user('test', 'test@magic.com',
                                                 'magic')
        u.user_permissions.set([perms.VIEW_PERM, perms.EDIT_PERM])

        p2 = create_part(2)
        p2.save()

        p3 = create_part(3)
        p3.save()

        w = create_warehouse(2, 'Assyhouse #1')
        w.save()
        v = create_vendor(2, 'Assyhouse #1')
        v.save()

        Access_Tests.setup_part()
        Access_Tests.setup_vendor()
        Access_Tests.setup_warehouse()

    # def test_redirect_to_https(self):
    #     url = reverse('epic:epic_index')
    #     response = self.client.get(url, secure=False)
    #     self.assertEqual(response.status_code, 301)
    #     self.assertIsNotNone(re.match('https://', response.url))

    def test_unauthenticated_accesses(self):
        response = self.client.get(reverse('epic:part_info') + '?pid=1',
                                   secure=True)
        self.assertEqual(response.status_code, 302)

        for url, _ in URL_Iterator(urls.urlpatterns):
            m = re.match(r'/epic/dal/', url)
            # auto-completion URLs respond with 403 (Forbidden)
            # others return redirect to login page:
            if m:
                expected = 403
            else:
                expected = 302
            response = self.client.get(url, secure=True)
            if response.status_code != expected:
                print('Unauthenticated access to %s returned %s, expected %s' %
                      (url, response.status_code, expected))
            self.assertEqual(response.status_code, expected)

    def test_authenticated_accesses(self):
        self.client.login(username='test', password='magic')

        for url, pattern_name in URL_Iterator(urls.urlpatterns):
            if re.match('order_', pattern_name):
                # order part 1 (an assembly) from vendor 'Assyhouse #1'
                o = create_order(1, 2, 1)
                o.save()
                l = create_line_item(1, 1, qty=13, line_cost='15.95', index=1)
                l.save()
            elif re.match('ship_', pattern_name):
                # order part 1(an assembly) from vendor 'Assyhouse #1'
                o = create_order(2, 2, 1)
                o.save()
                l = create_line_item(2, 1, qty=13, line_cost='15.95', index=1)
                l.save()
                s = create_ship(1, 2, 1, '1234,5678', 1.1, 2.2, 3.3)
                s.save()
                l = create_line_item(1, 1, qty=2, line_cost='2', index=1)
                l.save()
            elif re.match('warehouse_inventory_', pattern_name):
                i = create_inventory(1, 1)
                i.save()

            expect = 200
            expect_url = None
            post_action = []
            if pattern_name == 'search_results':
                url += '?q=1'
                # with a unique result, search redirects to the detail page:
                expect = 302
                expect_url = reverse('epic:part_detail', kwargs={'pk': 1})
            elif pattern_name == 'part_delete':
                expect = 302
                expect_url = reverse('epic:part_list')
                post_action.append(Access_Tests.setup_part)
            elif pattern_name == 'ship_delete':
                expect = 302
                expect_url = reverse('epic:ship_list')
            elif pattern_name == 'vendor_delete':
                expect = 302
                expect_url = reverse('epic:vendor_list')
                post_action.append(Access_Tests.setup_vendor)
            elif pattern_name == 'warehouse_delete':
                expect = 302
                expect_url = reverse('epic:warehouse_list')
                post_action.append(Access_Tests.setup_warehouse)
            elif pattern_name == 'warehouse_inventory_delete':
                expect = 302
                expect_url = reverse('epic:warehouse_detail', kwargs={'pk':1})
            elif pattern_name == 'order_delete':
                expect = 302
                expect_url = reverse('epic:order_list')
            elif pattern_name in ['datasheet_add_part', 'datasheet_detail',
                                  'datasheet_edit', 'datasheet_delete']:
                continue

            response = self.client.get(url, secure=True)

            # we can't only have one transaction with pk=1...
            Shipment.objects.all().delete()
            Order.objects.all().delete()
            Inventory.objects.all().delete()

            if response.status_code != expect:
                print('access to %s failed' % url)
                if hasattr(response, 'url'):
                    print(' redirect URL: %s' % response.url)
            self.assertEqual(response.status_code, expect)
            if expect_url is not None:
                url = response.url
                if url[-1] == '/':
                    url = url[:-1]
                self.assertEqual(url, expect_url)

            for action in post_action:
                action()

    def test_form_upload(self):
        self.client.login(username='test', password='magic')

        # Verify that we can post item formset to an order:
        response = self.client.post('/epic/order/add',
                                    {
                                        'ts': '2018-08-06',
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'This is a note',
                                        'line_item_set-TOTAL_FORMS': 12,
                                        'line_item_set-INITIAL_FORMS': 0,
                                        'line_item_set-MIN_NUM_FORMS': 0,
                                        'line_item_set-MAX_NUM_FORMS': 1000,
                                        'line_item_set-0-qty': 8,
                                        'line_item_set-0-part': 3,
                                        'line_item_set-0-vendor_pn': 'VPN #1',
                                        'line_item_set-0-part_cost': 1,
                                        'line_item_set-0-line_cost': 120,
                                        'line_item_set-1-qty': 18,
                                        'line_item_set-1-part': 2,
                                        'line_item_set-1-vendor_pn': 'VPN #2',
                                        'line_item_set-1-part_cost': 1,
                                        'line_item_set-1-line_cost': 121
                                    })

        # response.request['wsgi.input']._FakePayload__content.seek(0)
        # print('REQUEST',
        #       response.request['wsgi.input']._FakePayload__content.read()
        #       .decode('utf-8'))
        if response.status_code != 302:
            for name in response.context.keys():
                if re.match(r'[^_]+_form$', name):
                    print('%s.errors:' % name, response.context[name].errors)

        self.assertEqual(response.status_code, 302)
        order = Order.objects.all()[0]
        line_items = Line_Item.objects.filter(txtn=order)
        self.assertEqual(line_items[0].qty, 8)
        self.assertEqual(line_items[0].part.id, 3)
        self.assertEqual(line_items[0].line_cost, 120)
        self.assertEqual(line_items[1].qty, 18)
        self.assertEqual(line_items[1].part.id, 2)
        self.assertEqual(line_items[1].line_cost, 121)

        # Verify that we can post an item formset to a shipment:
        response = self.client.post('/epic/order/%d/add-shipment' % order.id,
                                    {
                                        'ts': '2018-08-06',
                                        'cost_freight': 10.0,
                                        'cost_other': 3.14,
                                        'cost_discount': 2.74,
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'note 2',
                                        'line_item_set-TOTAL_FORMS': 12,
                                        'line_item_set-INITIAL_FORMS': 0,
                                        'line_item_set-MIN_NUM_FORMS': 0,
                                        'line_item_set-MAX_NUM_FORMS': 1000,
                                        'line_item_set-0-qty': 7,
                                        'line_item_set-0-part': 3,
                                        'line_item_set-0-vendor_pn': 'VPN #1',
                                        'line_item_set-0-part_cost': 1,
                                        'line_item_set-0-line_cost': 120,
                                        'line_item_set-1-qty': 17,
                                        'line_item_set-1-part': 2,
                                        'line_item_set-1-vendor_pn': 'VPN #2',
                                        'line_item_set-1-part_cost': 1,
                                        'line_item_set-1-line_cost': 121
                                    })
        self.assertEqual(response.status_code, 302)
        ship = Shipment.objects.all()[0]
        line_items = Line_Item.objects.filter(txtn=ship)
        self.assertEqual(line_items[0].qty, 7)
        self.assertEqual(line_items[0].part.id, 3)
        self.assertEqual(line_items[0].line_cost, 120)
        self.assertEqual(line_items[1].qty, 17)
        self.assertEqual(line_items[1].part.id, 2)
        self.assertEqual(line_items[1].line_cost, 121)

        # Verify that we can post a delta formset as an inventory:
        response = self.client.post('/epic/warehouse/1/add-inventory',
                                    {
                                        'ts': '2018-08-06 08:15 AM',
                                        'notes': 'Note #3',
                                        'delta_set-TOTAL_FORMS': 12,
                                        'delta_set-INITIAL_FORMS': 0,
                                        'delta_set-MIN_NUM_FORMS': 0,
                                        'delta_set-MAX_NUM_FORMS': 1000,
                                        'delta_set-0-adj': 6,
                                        'delta_set-0-part': 2,
                                        'delta_set-0-vendor_pn': 'VPN #2',
                                        'delta_set-0-part_cost': 1,
                                        'delta_set-0-line_cost': 120,
                                        'delta_set-1-adj': 16,
                                        'delta_set-1-part': 3,
                                        'delta_set-1-vendor_pn': 'VPN #3',
                                        'delta_set-1-part_cost': 1,
                                        'delta_set-1-line_cost': 121
                                    })
        self.assertEqual(response.status_code, 302)
        inv = Inventory.objects.all()[0]
        line_items = Delta.objects.filter(txtn=inv)
        self.assertEqual(line_items[0].part.id, 2)
        self.assertEqual(line_items[0].is_absolute, True)
        self.assertEqual(line_items[0].adj, 6)
        self.assertEqual(line_items[0].warehouse.id, 1)
        self.assertEqual(line_items[1].part.id, 3)
        self.assertEqual(line_items[1].is_absolute, True)
        self.assertEqual(line_items[1].adj, 16)
        self.assertEqual(line_items[1].warehouse.id, 1)

    def test_spreadsheet_upload(self):
        self.client.login(username='test', password='magic')
        # import pdb
        # pdb.set_trace()

        # Verify invalid column names yield the expected errors:
        response = self.client.post('/epic/order/add',
                                    {
                                        'ts': '2018-08-06',
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'This is a note',
                                        'spreadsheet': io.BytesIO(
                                            b'qty1,part1,extra,amount1\n'
                                            b'10,3,5,99\n'
                                            b'40,EP0002,,10\n'),
                                        'line_item_set-TOTAL_FORMS': 0,
                                        'line_item_set-INITIAL_FORMS': 0
                                    })
        self.assertFormError(response, 'order_form', 'spreadsheet',
                             ['Column qty missing.  It may also be called '
                              'one of: quantity, count.',
                              'Column part missing.  It may also be called '
                              'one of: pn, ep.',
                              'Column amount missing.  It may also be called '
                              'one of: cost.'])

        # Verify that we can post a CSV file to an order:
        response = self.client.post('/epic/order/add',
                                    {
                                        'ts': '2018-08-06',
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'This is a note',
                                        'spreadsheet': io.BytesIO(
                                            b'qty,part,extra,amount\n'
                                            b'10,3,5,99\n'
                                            b'40,EP0002,,10\n'),
                                        'line_item_set-TOTAL_FORMS': 0,
                                        'line_item_set-INITIAL_FORMS': 0
                                    })
        self.assertEqual(response.status_code, 302)
        order = Order.objects.all()[0]
        line_items = Line_Item.objects.filter(txtn=order)
        self.assertEqual(line_items[0].qty, 10)
        self.assertEqual(line_items[0].part.id, 3)
        self.assertEqual(line_items[0].line_cost, 99)
        self.assertEqual(line_items[1].qty, 40)
        self.assertEqual(line_items[1].part.id, 2)
        self.assertEqual(line_items[1].line_cost, 10)

        # Verify that we can post a CSV file to a shipment:
        response = self.client.post('/epic/order/%d/add-shipment' % order.id,
                                    {
                                        'ts': '2018-08-06',
                                        'cost_freight': 10.0,
                                        'cost_other': 3.14,
                                        'cost_discount': 2.74,
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'note 2',
                                        'spreadsheet': io.BytesIO(
                                            b'qty,part,extra,amount\n'
                                            b'5,3,5,99\n'
                                            b'10,EP0002,,10\n')
                                    })
        self.assertEqual(response.status_code, 302)
        ship = Shipment.objects.all()[0]
        line_items = Line_Item.objects.filter(txtn=ship)
        self.assertEqual(line_items[0].qty, 5)
        self.assertEqual(line_items[0].part.id, 3)
        self.assertEqual(line_items[0].line_cost, 99)
        self.assertEqual(line_items[1].qty, 10)
        self.assertEqual(line_items[1].part.id, 2)
        self.assertEqual(line_items[1].line_cost, 10)

        # Verify that we can post an Excel file as an inventory:
        excel_file = os.path.join(os.path.dirname(epic.__file__),
                                  'test-data', 'inventory.xlsx')
        with open(excel_file, 'rb') as f:
            response = self.client.post('/epic/warehouse/1/add-inventory',
                                        {
                                            'ts': '2018-08-06 08:15 AM',
                                            'spreadsheet': f
                                        })
        self.assertEqual(response.status_code, 302)
        inv = Inventory.objects.all()[0]
        line_items = Delta.objects.filter(txtn=inv)
        self.assertEqual(line_items[0].part.id, 2)
        self.assertEqual(line_items[0].is_absolute, True)
        self.assertEqual(line_items[0].adj, 1234)
        self.assertEqual(line_items[0].warehouse.id, 1)
        self.assertEqual(line_items[1].part.id, 3)
        self.assertEqual(line_items[1].is_absolute, True)
        self.assertEqual(line_items[1].adj, 5678)
        self.assertEqual(line_items[1].warehouse.id, 1)

    def test_order_status(self):
        '''Confirm that deleting a shipment for completed order will mark
        order as OPEN again.

        '''
        self.client.login(username='test', password='magic')
        # create order from Vendor #1:
        o = create_order(pk=4, vendor_id=1, warehouse_id=1)
        o.save()
        l = create_line_item(o.pk, 2, qty=1000, line_cost='256.35', index=1)
        l.save()

        # order should be open now
        self.assertEqual(o.status, Enum.TXTN_STATUS_OPEN)

        # create two shipments that should complete the order:
        s = create_ship(2, o.pk, 1, '1Z1345', 1.23, 2.34, 3.45)
        s.save()
        l = create_line_item(s.pk, 2, 900, line_cost='230.05', index=1)
        l.save()

        s = create_ship(3, o.pk, 1, '1Z1356', 1.23, 2.34, 3.45)
        s.save()
        l = create_line_item(s.pk, 2, 100, line_cost='230.05', index=1)
        l.save()

        s.finalize()

        # order should be closed now:
        o = Order.objects.get(pk=4)
        self.assertEqual(o.status, Enum.TXTN_STATUS_CLOSED)

        # delete the first shipment:
        url = reverse('epic:ship_delete', kwargs={'pk':2})
        response = self.client.get(url, secure=True)
        self.assertEqual(response.status_code, 302)

        # order should be open again:
        o = Order.objects.get(pk=4)
        self.assertEqual(o.status, Enum.TXTN_STATUS_OPEN)
