#
#    Copyright (C) 2002-2014  Corporation of Balclutha. All rights Reserved.
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
#    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
#    GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
#    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
#    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
#    OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

# import stuff
import AccessControl, os, types, operator, Acquisition, string, sys, traceback
from AccessControl import getSecurityManager
from AccessControl.Permissions import view_management_screens, manage_properties, \
    access_contents_information
from AccessControl.users import BasicUser
from OFS.userfolder import manage_addUserFolder
from DateTime import DateTime
from OFS.PropertyManager import PropertyManager
from OFS.ObjectManager import BeforeDeleteException
from Acquisition import aq_base
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
import logging

from Products.BastionBanking.ZCurrency import ZCurrency, CURRENCIES
from Products.BastionBanking import ZReturnCode

from utils import floor_date, ceiling_date, assert_currency
from BLBase import *
from BLAccount import BLAccounts, date_field_cmp, addBLAccount
from BLSubsidiaryLedger import BLSubsidiaryLedger
from BLEntry import manage_addBLEntry
from BLSubsidiaryEntry import manage_addBLSubsidiaryEntry
from BLInventory import BLInventory
from BLSubsidiaryAccount import BLSubsidiaryAccount
from Products.CMFCore import permissions
from Products.CMFCore.utils import getToolByName

from Permissions import ManageBastionLedgers, OperateBastionLedgers, OverseeBastionLedgers
from BLAttachmentSupport import BLAttachmentSupport
from BLProcess import manage_addBLProcess
from BLTransactionTemplate import manage_addBLTransactionTemplate
from BLEntryTemplate import manage_addBLEntryTemplate
from Exceptions import AlreadyPostedError, PostingError, LedgerError, InvalidState

from zope.interface import implements
from interfaces.inventory import ICashBook, IOrder


LOG = logging.getLogger('BLOrderBook')

def addBLOrderBook(self, id, title='', REQUEST=None):
    """
    Plone-based entry
    """
    inventory_id = self.bastionLedger().objectIds('BLInventory')[0]
    control = addBLAccount(self.Ledger, id, title)
    self.manage_addBLOrderBook(id, control, inventory_id, 'Q', title)
    return id

manage_addBLOrderBookForm = PageTemplateFile('zpt/add_orderbook', globals()) 
def manage_addBLOrderBook(self, id, controlAccount, inventory,
                          prefix='Q', title='', REQUEST=None):
    """ adds an orderbook """
    try:
        # do some checking ...
        if type(controlAccount) == types.StringType:
            control = self.Ledger._getOb(controlAccount)
        else:
            control = controlAccount
        assert control.meta_type =='BLAccount', "Incorrect Control Account Type - Must be strictly GL"
        assert not getattr(aq_base(control), id, None),"A Subsidiary Ledger Already Exists for this account."

        self._setObject(id, BLOrderBook(id, title, control, inventory,
                                        self.Ledger.currencies, prefix))
    except:
        # TODO: a messagedialogue ..
        raise
    
    if REQUEST is not None:
        return self.manage_main(self, REQUEST)

class BLOrderBook (BLSubsidiaryLedger):
    """ """
    meta_type = portal_type = 'BLOrderBook'

    __ac_permissions__ = BLSubsidiaryLedger.__ac_permissions__ + (
        (access_contents_information, ('orderValues', 'orderStatuses')),
        (view_management_screens, ('manage_accounts', 'manage_transactions', 'manage_Reports', 
                                   'manage_orders',)),
        (OperateBastionLedgers, ('edit', 'nextOrderNo', 'manage_postOrder', )),
        (ManageBastionLedgers, ('manage_edit',)),
	(view, ('isReceivable', 'isPayable', 'blInventory', 'totalTaxInclusiveAmount',
		'totalTaxExclusiveAmount', 'totalAmount', )),
        )

    _properties = BLSubsidiaryLedger._properties + (
        {'id':'inventory',    'type':'selection', 'mode':'w', 'select variable':'InventoryIds'},
        {'id':'instructions', 'type':'text',       'mode':'w'},
        )

    account_types = ('BLOrderAccount',)

    # hmmm - a late addition ...
    instructions = ''
    
    manage_options = BLSubsidiaryLedger.manage_options[0:4] + (
        {'label':'Properties', 'action':'manage_propertiesForm',
         'help':('BastionLedger', 'orderbook.stx')},
    ) + BLSubsidiaryLedger.manage_options[5:]

    def InventoryIds(self):
        return map(lambda x: x.getId(), self.inventoryValues())

    # we've got a required macro here
    manage_ledgerPropsForm = BLSubsidiaryLedger.manage_propertiesForm

    manage_orders = PageTemplateFile('zpt/view_orders', globals())
    manage_propertiesForm = PageTemplateFile('zpt/orderbook_props', globals())

    def __init__(self, id, title, control, inventory, currencies, order_prefix='Q',
                 account_prefix='A', txn_prefix='T' ):
        self.order_prefix = order_prefix
        self.order_no = 1
        # think inventory is just the id of the BLInventory object ...
        #assert isinstance(inventory, BLInventory), "Incorrect Inventory Type"
        self.inventory = inventory
        BLSubsidiaryLedger.__init__(self, id, title, (control,), currencies,
                                    1000000, account_prefix, 1, txn_prefix)
        self.instructions = ''

    def orderStatuses(self):
        """
        return a list of status ids associated with order workflow
        """
        return getToolByName(self, 'portal_workflow').blorder_workflow.states.objectIds()

        
    def orderValues(self, entered_by=[], orderdate=[], status=[], **kw):
        """
        returns a list of BLOrder objects meeting the criteria
        """
        orders = []
        for account in self.accountValues(**kw):
            orders.extend(account.orderValues(entered_by, orderdate, status))
        return orders

    def isReceivable(self):
        return self.accountType() == 'Asset'

    def isPayable(self):
        return self.accountType() == 'Liability'

    def manage_edit(self, title, description, txn_id, account_id, account_prefix,
                    txn_prefix, currencies, email='',  REQUEST=None):
        """
        update orderbook properties
        """
        # do some bodgy stuff here ...
        request = self.REQUEST
        
        self.inventory = request.get('inventory', self.InventoryIds()[0])
        self.instructions = request.get('instructions', '')

        return BLSubsidiaryLedger.manage_edit(self, title, description, txn_id, account_id, account_prefix,
                                              txn_prefix, currencies, email, REQUEST)
    

    def nextOrderNo(self):
        """
        generate the order number
        """
        id = str(self.order_no)
        self.order_no += 1
        return "%s%s" % (self.order_prefix, string.zfill(id, 10))

    def blInventory(self):
        """
        return the Inventory from which to order parts against
        """
        try:
            return getattr(self.aq_parent, self.inventory)
        except:
            raise AttributeError, 'Inventory not found: %s' % self.inventory

    def manage_postOrder(self, order, REQUEST=None):
        """
        take an order and turn it into a transaction and post it ...
        """
        if order.getNet() == 0:
            return

        #
        # ignore Process driver - call txn directly
        #
        transaction = self.blp_order.generate(order.blAccount(),
                                              order,
                                              title = order.title,
                                              effective=floor_date(order.orderdate))

        # this shouldn't happen but we're getting in all kinds of crap posting cashbook orders
        # via manage_pay ...
        if not transaction.status() == 'posted':
            try:
                transaction.manage_post()
            except AlreadyPostedError:
                pass

        for orderitem in order.objectValues('BLOrderItem'):
            #
            # fix the price in the order - it is floating based on inventory until then ...
            #
            orderitem.amount = orderitem.calculateNetPrice()
            orderitem.blPart().onhand =- orderitem.quantity
        
        # assign txn to order for backwards reference ...
        order.txn_id = transaction.getId()
        transaction.setReference(order)

        if REQUEST:
            return self.manage_main(self, REQUEST)

    def totalTaxInclusiveAmount(self, datemin, datemax):
        obs = map ( lambda x: x.getObject(),
                    self.catalog( orderdate = { 'query': [datemin, datemax], 'range':'minmax' },
                                               status = {'query': 'invoiced' } ) )
        if obs:
            return reduce( operator.add, map ( lambda x: x.getGross(),
                                               filter(lambda x: x.taxincluded, obs) ) )
        else:
            return ZCurrency(self.currencies[0], 0)


    def totalTaxExclusiveAmount(self, datemin, datemax):
        obs = map ( lambda x: x.getObject(),
                    self.catalog( orderdate = { 'query': [datemin, datemax], 'range':'minmax' },
                                               status = {'query': 'invoiced' } ) )

        if obs:
            return reduce( operator.add, map ( lambda x: x.getGross(),
                                               filter(lambda x: not x.taxincluded, obs) ))          
        else:
            return ZCurrency(self.currencies[0], 0)

    def totalAmount(self, datemin, datemax):
        LOG.debug( "BLOrderBook::totalAmount() cat = %s" % str( self.catalog( status = {'query':'invoiced'} ) ))
        amts = map ( lambda x: x.getObject().getGross(),
                    self.catalog(orderdate = { 'query': [datemin, datemax], 'range':'minmax' },
                                 status = {'query':'invoiced'} ))

        LOG.debug( "BLOrderBook::totalAmount(%s, %s) amts = %s" % ( datemin, datemax, str(amts) ))
        if amts:
            return reduce( operator.add, amts )          
        else:
            return ZCurrency(self.currencies[0], 0)

    def _repair(self):
        BLSubsidiaryLedger._repair(self)
        # new FSProcess-based postOrder ...
        if self.__dict__.has_key('_reserved_names'):
            del self.__dict__['_reserved_names']
        # hmmm - if we've ZODB'd our propmap ...
        if not self.hasProperty('instructions'):
            self._properties=self._properties + (
                {'id':'instructions', 'type':'text', 'mode':'w'},)
        if not self.hasProperty('inventory'):
            self._properties = self._properties + (
                {'id':'inventory', 'type':'selection', 'mode':'w', 'select variable':'InventoryIds'},)
            self.inventory = self.aq_parent.objectIds('BLInventory')[0]

AccessControl.class_init.InitializeClass(BLOrderBook)


def addBLOrderAccount(self, id, title='', REQUEST=None):
    """
    Plone adding of order account from ledger
    """
    id = self.manage_addBLOrderAccount(id=id, title=title)
    return id
    
manage_addBLOrderAccountForm = PageTemplateFile('zpt/add_orderaccount', globals()) 
def manage_addBLOrderAccount(self, id='', title='', description='', currency='', uid='', pwd='', 
                             email='', contact='', address='', phone='', fax='', discount=0.0, 
                             creditlimit=0, terms=7, notes='', shiptoname='', shiptoaddress='', 
                             shiptocontact='', shiptophone='', shiptofax='', shiptoemail='', 
                             taxincluded=False, REQUEST=None):
    """
    a customer/vendor

    if uid is a User, then change ownership of the account
    if uid/pwd present, then set up an acl_users and change account ownership
    """
    #assert isinstance(self.this(), BLAccounts), 'Wrong Container Type: %s' % self.meta_type
    if not currency:
        currency = self.defaultCurrency()
    assert currency in CURRENCIES, 'Unknown currency: %s' % currency
    if not id:
        id = self.nextAccountId()

    self._setObject(id, BLOrderAccount(id, title, description, self.accountType(), currency, email, 
                                       contact, address, phone, fax, discount, taxincluded, terms, 
                                       creditlimit, notes, shiptoname, shiptoaddress, 
                                       shiptocontact, shiptophone, shiptofax, shiptoemail))

    account = self._getOb(id)

    if uid:
        if type(uid) == types.StringType and pwd:
            manage_addUserFolder(account)
            account.acl_users.userFolderAddUser(uid, pwd, [], [])
            user = account.acl_users.getUser(uid)
        else:
            assert isinstance(uid, BasicUser), 'uid not a User!!'
            user = uid
        account.changeOwnership(user)
        account.manage_setLocalRoles(id, ['Owner'])

    bt = getToolByName(self, 'portal_bastionledger')
    if bt.hasTaxTable('sales_tax'):
        account.manage_addTaxGroup('sales_tax')

    if REQUEST is not None:
        REQUEST.RESPONSE.redirect("%s/%s/manage_details" % (REQUEST['URL3'], id))

    return id


class BLOrderAccount(BLSubsidiaryAccount):
    """
    This class encompasses the concept of an orderbook's accounts.

    These accounts have additional information necessary to ship goods and
    include facilities to retain payment information.
    
    """
    meta_type = portal_type = 'BLOrderAccount'

    __ac_permissions__ = BLSubsidiaryAccount.__ac_permissions__ + (
        (access_contents_information, ('orderValues', )),
        (view_management_screens, ('manage_statement', 'getOrder',
                                   'acl_item', )),
        (OperateBastionLedgers, ('manage_invoiceObjects','manage_finishObjects',
				 'generateUniqueId','manage_addOrder',
                                 'manage_oaEdit')),
        (OverseeBastionLedgers, ('manage_cancelObjects',)),
	(view, ('entries', 'manage_payAccount', 'contactEmail')),
        )

    _properties = BLSubsidiaryAccount._properties + (
        {'id':'contact',       'type':'string',   'mode':'w'},
        {'id':'address',       'type':'text',     'mode':'w'},
        {'id':'phone',         'type':'string',   'mode':'w'},
        {'id':'fax',           'type':'string',   'mode':'w'},
        {'id':'discount',      'type':'float',    'mode':'w'},
        {'id':'creditlimit'  , 'type':'currency', 'mode':'w'},
        {'id':'taxincluded',   'type':'boolean',  'mode':'w'},
        {'id':'terms',         'type':'int',      'mode':'w'},
        {'id':'notes',         'type':'text',     'mode':'w'},
        {'id':'shiptoname',    'type':'string',   'mode':'w'},
        {'id':'shiptoaddress', 'type':'text',     'mode':'w'},
        {'id':'shiptocontact', 'type':'string',   'mode':'w'},
        {'id':'shiptophone',   'type':'string',   'mode':'w'},
        {'id':'shiptofax',     'type':'string',   'mode':'w'},
        {'id':'shiptoemail',   'type':'string',   'mode':'w'},
        )

    def manage_options(self):
	options = [
        {'label': 'Statement',     'action': 'manage_statement',
          'help':('BastionLedger', 'orderaccount.stx') },
        {'label': 'View',  'action': '',},
        {'label': 'Orders',  'action': 'manage_main',
         'help':('BastionLedger', 'orderstmt.stx') },
        {'label': 'Details',    'action': 'manage_details' } ]
	options.extend( BLSubsidiaryAccount.manage_options(self)[3:])
	return tuple(options)

    #index_html = PageTemplateFile('zpt/orderaccount_index', globals())
    manage_main = PageTemplateFile('zpt/view_orders', globals())
    manage_details   = PageTemplateFile('zpt/edit_orderaccount', globals())

    def filtered_meta_types(self, user=None):
        if self.status() in ('open', 'healthy'):
	    return [ ProductsDictionary('BLOrder') ]
        return []

    def __init__(self, id, title, description, type, currency, email='', contact='', address='',
                 phone='', fax='', discount=0, taxincluded=False, terms=7, creditlimit=None, 
                 notes='', shiptoname='', shiptoaddress='', shiptocontact='', shiptophone='', 
                 shiptofax='', shiptoemail=''):
        BLSubsidiaryAccount.__init__(self, id, title, description, type, 'OrderBook', currency, id)
        self.opened = DateTime()
	if not creditlimit:
	    creditlimit = ZCurrency(currency, 0)
        BLOrderAccount.manage_oaEdit(self, title, description, email, contact, address,  phone, fax, discount,
                                     creditlimit, terms, notes, shiptoname, shiptoaddress,
                                     shiptocontact, shiptophone, shiptofax, shiptoemail, taxincluded)

    def manage_oaEdit(self, title, description, email, contact, address,  phone, fax, discount=0,
                      creditlimit=0, terms=7, notes='', shiptoname='', shiptoaddress='',
                      shiptocontact='', shiptophone='', shiptofax='', shiptoemail='',
                      taxincluded=False, REQUEST=None):
        """ 
        """
        self.title = title
        self.description = description
        self.email = email
        self.contact = contact
        self.address = address
        self.phone = phone
        self.fax = fax
        self.discount = float(discount)
        self.taxincluded = bool(taxincluded)
        self.terms = int(terms)
	assert creditlimit.__class__.__name__ == 'ZCurrency', 'creditlimit not a ZCurrency: %s' % creditlimit.__class__.__name__
        self.creditlimit = creditlimit
        self.notes = notes
        self.shiptoname = shiptoname 
        self.shiptoaddress = shiptoaddress
        self.shiptocontact = shiptocontact
        self.shiptophone = shiptophone
        self.shiptofax = shiptofax
        self.shiptoemail = shiptoemail
        self.bank_account = ''

        if REQUEST:
            REQUEST.set('management_view', 'Details')
            REQUEST.set('manage_tabs_message', 'Updated')
            return self.manage_details(self, REQUEST)

    def entries(self, effective=[], status=['posted',]):
	"""
	just return active entries as this is a user-account and we don't want them
	to see all the cancellations/reversals etc ...
	"""
	return self.entryValues(effective, status)

    # inherit these!!
    def X__str__(self):
        return str(self.__dict__)

    def X___repr__(self):
        return str(self.__dict__)

        
    def manage_invoiceObjects(self, ids=[], REQUEST=None):
        """ """
        for id in ids:
            order = self._getOb(id)
	    if order.status() == 'open':
                order.manage_invoice()
            
        if REQUEST:
            return self.manage_main(self, REQUEST)

    def manage_finishObjects(self, ids=[], REQUEST=None):
        """
        go complete these orders - this may mean payment(s) have been assigned etc etc
        """
        for id in ids:
            order = self._getOb(id)
            try:
                order.content_status_modify(workflow_action='finish')
            except:
                pass

        if REQUEST:
            return self.manage_main(self, REQUEST)

    def manage_delObjects(self, ids=[], force=0, REQUEST=None):
        """ """
        # only remove incomplete orders (or BLEntry's - as allowed by BLAccount...)
        if type(ids) == types.StringType:
            ids = [ ids ]
        for ob in map(lambda x,y=self: y._getOb(x), ids):
            if force:
                self._delObject(ob.getId(), force=force)
            else:
                #if not isinstance(ob, BLOrder) or ob.status() in ('incomplete', 'uninvoiced'):
                self._delObject(ob.getId())
        if REQUEST:
            REQUEST.set('manage_tabs_message', 'Deleted')
            return self.manage_main(self, REQUEST)

    def manage_cancelObjects(self, ids=[], REQUEST=None):
        """ """
        for id in ids:
            self._getOb(id).manage_cancel()
            
        if REQUEST is not None:
            return self.manage_main(self, REQUEST)

    def manage_payAccount(self, amount=None, REQUEST=None):
        """
        create a payment transaction, but don't actually post it
        this is needed for BastionMerchantService's which do asynchronous
        RPC :(
        """
        if not amount:
            amount = self.balance()
        try:
            if type(amount) == types.StringType:
                amount = ZCurrency(self.base_currency, float(amount))
        except Exception, e:
            message = "Not a valid amount: %s" % e
            if REQUEST is not None:
                REQUEST.set('manage_tabs_message', message)
                return self.manage_statement(self, REQUEST)
            raise AttributeError, message

        if amount != 0:
            BLSubsidiaryAccount.manage_payAccount(self, 
                                                  amount,
                                                  self.Ledger.accountValues(tags='order_payments')[0])

        if REQUEST:
            REQUEST.set('manage_tabs_message', 'Payment Processed')
            return self.manage_main(self, REQUEST)
        
    def getOrder(self, txn_id):
        reference = self._getOb(txn_id).reference
        if reference is not None and getattr(self, reference, None):
            return getattr(self, reference)
        return None

    def manage_addOrder(self, title='', orderdate=None, reqdate=None, taxincluded=False, discount=0.0, 
                        REQUEST=None):
        """ adds an order - but without needing to use the BLOrder add page template ..."""
        id = self.aq_parent.nextOrderNo()
        self._setObject(id, BLOrder(id, 
                                    title, 
                                    orderdate=orderdate or DateTime(),
                                    reqdate=reqdate or DateTime() + 7,
                                    taxincluded=taxincluded, 
                                    discount=discount))
        order = self._getOb(id)
        if REQUEST:
            REQUEST.RESPONSE.redirect('%s/%s/manage_main' % (self.getId(), order.getId()))
        return order
    
    def acl_item(self):
        return getattr(aq_base(self), 'acl_users', None)

    def generateUniqueId(self, type_name=None):
        """
        TODO - future potential ...
        """
        if type_name in ['BLOrder', 'BLCashOrder']:
            return self.nextOrderNo()
        return BLSubsidiaryAccount.generateUniqueId(self, type_name)

    def orderValues(self,entered_by=[], orderdate=[], status=[], **kw):
        """
        returns a list of BLOrder objects meeting the criteria
        """
        orders = self.objectValues(('BLOrder', 'BLCashOrder'))

        if orderdate:
            orders = filter(lambda x: x.orderdate >= min(orderdate) and x.orderdate <= max(orderdate), orders)
            
        if status:
            orders = filter(lambda x: x.status() in status, orders)

        return orders

    def contactEmail(self):
        """
        the account owner's email address
        """
        return self.getProperty('email')

    def companyName(self):
        """
        """
        return self.Title()

    def billingAddress(self):
        """
        """
        return self.address

    def shippingAddress(self):
        """
        """
        return self.shiptoaddress

    def _repair(self):
        BLSubsidiaryAccount._repair(self)
        if not getattr(aq_base(self), 'notes', None):
            self.notes = ''
	if type(self.address) == type([]):
	    self.address = '\n'.join(self.address)
	if type(self.shiptoaddress) == type([]):
	    self.shiptoaddress = '\n'.join(self.shiptoaddress)
	if not type(self.creditlimit) == ZCurrency:
	    try:
	        self.creditlimit = ZCurrency(self.base_currency, self.creditlimit)
	    except:
	        # maybe it's our old Currency class ...
		self.creditlimit = ZCurrency(self.base_currency, 0)
        map( lambda x: x._repair(), self.objectValues('BLOrder') )

        # hmmm - we seem to have some old shite lying around ...
        if self.__dict__.has_key('_properties'):
            props = self.__dict__['_properties']
            del self.__dict__['_properties']
                
        if self.__dict__.has_key('__allow_groups__'):
            del self.__dict__['__allow_groups__']
            
AccessControl.class_init.InitializeClass(BLOrderAccount)

manage_addBLOrderItemForm = PageTemplateFile('zpt/add_orderitem', globals()) 
def manage_addBLOrderItem(self, part, title='', qty=1, unit=0, discount=0.0, amount=None, REQUEST=None):
    """ 
    an order line within an Order 
    
    you can specifically override the order discount on the orderline
    """

    realself = self.this()
    if not isinstance(realself, BLOrder):
        raise LedgerError, "Cannot add a BLOrderItem to this type: %s" % self.meta_type
    try:
        qty = float(qty)
    except:
        raise ValueError, """ Quantity is not numeric! """

    if discount > 100.0 or discount < 0.0:
        raise ValueError, """ Discount is expressed as a decimal! """
        
    if not getattr(part, 'meta_type', None):
        realpart = realself.blInventory().blPart(part)
    else:
	realpart = part

    assert realpart and realpart.meta_type == 'BLPart', "Part not found: %s" % part

    if not unit:
        if realself.isSell():
            unit = realpart.sellprice
        else:
            unit = realpart.listprice
            
    effective = realself.orderdate

    # do any FX adjustment on the unit price ...
    base_currency = self.aq_parent.base_currency
    if unit._currency != base_currency:
        if self.aq_parent.blLedger().isReceivable():
            rate = 1.0 / self.portal_bastionledger.crossBuyRate(base_currency, unit._currency, effective)
        else:
            rate = self.portal_bastionledger.crossSellRate(base_currency, unit._currency, effective)            
        unit = ZCurrency(base_currency, rate * unit._amount)

    if amount and amount._currency != base_currency:
        if self.aq_parent.blLedger().isReceivable():
            rate = 1.0 / self.portal_bastionledger.crossBuyRate(base_currency, amount._currency, effective)
        else:
            rate = self.portal_bastionledger.crossSellRate(base_currency, amount._currency, effective)            
        amount = ZCurrency(base_currency, amount._amount * rate)
    #
    # cycle thru some numbers until we find  a good one ...
    #
    not_added = 1
    if self.objectIds():
            id = int( max(self.objectIds() ) ) + 1
    else:
        id = 1
    while not_added:
        try:
            #LOG.debug('%s %f * %s' % (part, qty, unit))
            item = BLOrderItem(str(id), realpart, unit, title, qty, discount, amount)
            self._setObject(str(id), item)
            not_added = 0
        except:
            typ, val, tb = sys.exc_info()
            if str(typ) in ('BadRequest', 'Bad Request', 'zExceptions.BadRequest'):
                # TODO: a messagedialogue ..
                id += 1
            else:
                raise
        
    if REQUEST is not None:
        return self.manage_main(self, REQUEST)
    else:
        return self._getOb(str(id))

class BLOrderItem(PropertyManager, PortalContent):
    """
    An order item is expected to have either a part existing in the inventory,
    or to be primed with an amount
    """
    meta_type = portal_type = 'BLOrderItem'

    __ac_permissions__ = PropertyManager.__ac_permissions__ + (
        (OperateBastionLedgers, ('manage_edit', 'setReference',)),
        (view, ('getReference', 'calculateGrossPrice', 'calculateNetPrice',
                'calculateCost', 'blPart', 'getReference',)),
        ) + PortalContent.__ac_permissions__

    _properties = PropertyManager._properties + (
        {'id':'part_id',       'type':'string',   'mode': 'r',},
        {'id':'unit',          'type':'currency', 'mode': 'w',},
        {'id':'quantity',      'type':'float',    'mode': 'w',},
        {'id':'discount',      'type':'float',    'mode': 'w',},
        {'id':'amount',        'type':'currency', 'mode': 'w',},
        {'id':'note',          'type':'text',     'mode': 'w',},
        {'id':'ref',           'type':'string',   'mode': 'w',},
        )
	
    ref = ''
    manage_options = PropertyManager.manage_options + PortalContent.manage_options

    manage_main = PropertyManager.manage_propertiesForm
    manage_main._setName('manage_propertiesForm')

    def __init__(self, id, part, unit, title='', quantity=1.0, discount=0.0, amount=None):
        self.id = id
        if title == '':
            title = part.title_and_id()
        try:
            self.part_id = part.getId()
        except:
            self.part_id = None
	self.ref = ''
        self.manage_edit(title, quantity, discount, unit, amount, '')

    def manage_edit(self, title, quantity, discount=0.0, unit=None, amount=None, note='', REQUEST=None):
        """
        """
        self.title = title
        self.quantity = abs(float(quantity))
        self.discount = abs(float(discount) / 100.0)
        if unit:
            try:
                assert_currency(unit)
            except:
                unit = ZCurrency(unit)
            self.unit = unit
        else:
            self.unit = None
        if amount:
            self.amount = abs(amount)
        else:
            self.amount = None
	self.note = note
        if REQUEST is not None:
            return self.manage_main(self, REQUEST)
        return 0

    def calculateNetPrice(self):
        """
        return price with discounts/price policy applied
        """
        if getattr(aq_base(self), 'amount', None):
            return self.amount

        account = self.aq_parent.blAccount()

        # acquire parent discount (if set)
        discount = self.discount or self.aq_parent.discount

        return self.calculateGrossPrice() * (1.0 - discount)

    def calculateGrossPrice(self):
        """
        return total undiscounted price
        """
        return self.unit * self.quantity
        
    def calculateCost(self):
        """
        work out the cost of an order item 
        """
        return self.blPart().listprice * self.quantity

    def blPart(self):
        """ orderitem in order in account in orderbook .... - unless it's in a BLQuote..."""
        return self.aq_parent.blInventory().blPart(self.part_id)

    def setReference(self, ref):
        """
        optionally store a reference to something
        """
        self.ref = ref

    def getReference(self):
        """
        return the object referenced or the reference itself
        """
        if getattr(aq_base(self), 'ref', None):
            try:
                return self.restrictedTraverse(self.ref)
            except:
                return self.ref
        return None

    def getIcon(self, relative_to_portal=False):
        return self.blPart().getIcon(relative_to_portal)

    def _repair(self):
        if not getattr(aq_base(self), 'unit', None):
            self.unit = self.blPart().sellprice
        if not getattr(aq_base(self), 'note', None):
	    self.note = ''
        amount = getattr(aq_base(self), 'amount', None)
        if amount:
            try:
                assert_currency(amount)
            except:
                try:
                    self.amount = ZCurrency(amount)
                except TypeError:
                    # hmmm - it's really f**ked!!
                    self.amount = ZCurrency(self.aq_parent.aq_parent.base_currency, 0)

AccessControl.class_init.InitializeClass(BLOrderItem)

manage_addBLOrderForm = PageTemplateFile('zpt/add_order', globals())                                         
def manage_addBLOrder(self, id='', title='', orderdate=DateTime(), taxincluded=False, 
                      notes='', company='', contact='', email='', address='', shiptoaddress='',
                      discount=0.0, REQUEST=None):
    """
    adds an order (Plonfied)
    """
    if not id:
        id = self.nextOrderNo()

    self._setObject(id, BLOrder(id, 
                                title, 
                                orderdate=orderdate, 
                                taxincluded=taxincluded, 
                                discount=discount,
                                notes=notes,
                                contact=contact,
                                company=company,
                                email=email,
                                address=address,
                                shiptoaddress=shiptoaddress))
 
    if REQUEST is not None:
        REQUEST.RESPONSE.redirect("%s/%s/manage_workspace" % (REQUEST['URL3'], id ))

    # Plone requires this ...
    return id

manage_addBLCashOrderForm = PageTemplateFile('zpt/add_cashorder', globals())                                         
def manage_addBLCashOrder(self, id='', title='', orderdate=DateTime(), taxincluded=False, 
                          discount=0.0, notes='', REQUEST=None):
    """
    adds an order (Plonfied)

    Note we automagically figure out if it should be cash or charge ...
    """
    if not id:
        id = self.nextOrderNo()

    self._setObject(id, 
                    BLCashOrder(id, title, orderdate=orderdate, 
                                taxincluded=taxincluded, discount=discount, 
                                notes=notes))

    if REQUEST is not None:
        REQUEST.RESPONSE.redirect("%s/%s/manage_workspace" % (REQUEST['URL3'], id ))

    # Plone requires this ...
    return id


def addBLOrder(self, id, title=''):
    """
    Plone order creation from an order account
    """
    id = self.manage_addBLOrder(id=id,
                                title=title,
                                orderdate=DateTime(),
                                taxincluded=self.taxincluded,
                                notes=self.instructions,
                                company=self.companyName(),
                                email=self.contactEmail(),
                                contact=self.contact,
                                address=self.billingAddress(),
                                shiptoaddress=self.shippingAddress())
    return id

def addBLCashOrder(self, id, title=''):
    """
    Plone cash order creation
    """
    id = manage_addBLCashOrder(self,
                               id=id,
                               title=title,
                               orderdate=DateTime(),
                               taxincluded=self.taxincluded,
                               notes=self.instructions)
    return id

class BLOrder(PortalFolder, PropertyManager, BLAttachmentSupport):
    """
    A purchase/sales request
    """    
    meta_type = portal_type = 'BLOrder'

    implements(IOrder)

    __ac_permissions__ = PortalFolder.__ac_permissions__ + (
        (access_contents_information, ('blTransaction', 'blAccount')),
        (view, ('orderItemValues', 'modifiable', 'index_html', 
		'getGross', 'getNet', 'getTax', 'getDiscount', 'calculateTax', 'status', 
		'modifiable', 'modificationTime', 'availableParts', 
		'isOpen', 'blTransaction', 'weight', 'isBuy', 'isSell','contactEmail')),
        (OperateBastionLedgers, ('manage_edit', 'setStatus', 'manage_status_modify',
				 'manage_emailInvoice', 'manage_invoice', 'manage_finish')),
        (OverseeBastionLedgers, ('manage_cancel',)),
        ) + PropertyManager.__ac_permissions__ + BLAttachmentSupport.__ac_permissions__

    company = ''
    billingaddress = ''
    shiptophone = ''
    shiptofax = ''
    
    _properties = PropertyManager._properties + (
        {'id':'orderdate',      'type':'date',    'mode':'w'},
        {'id':'reqdate',        'type':'date',    'mode':'w'},
        {'id':'notes',          'type':'text' ,   'mode':'w'},
	{'id':'taxincluded',    'type':'boolean', 'mode':'w'},
        {'id':'discount',       'type':'float',   'mode':'w'},
	{'id':'company',        'type':'string',  'mode':'w'},
	{'id':'contact',        'type':'string',  'mode':'w'},
	{'id':'email',          'type':'string',  'mode':'w'},
	{'id':'billingaddress', 'type':'text',    'mode':'w'},
	{'id':'shiptoaddress',  'type':'text',    'mode':'w'},
        {'id':'shiptophone',    'type':'string',  'mode':'w'},
        {'id':'shiptofax',      'type':'string',  'mode':'w'},
    )

    #__allow_access_to_unprotected_subobjects__ = 1
    
    manage_options = ({'label':'Contents', 'action':'manage_main'},
                      {'label':'Properties', 'action':'manage_propertiesForm',},
                      {'label':'View', 'action':''}) + \
                     BLAttachmentSupport.manage_options

    manage_main = PageTemplateFile('zpt/view_order', globals())

    # debug ...
    manage_btree = PortalFolder.manage_main
    
    def __init__(self, id, title, discount=0.0, taxincluded=0, notes='', address='', shiptoaddress='',
                 company='', contact='', email='', orderdate=DateTime(), reqdate=DateTime() + 7):
        self.id = id
        self.manage_edit(title, orderdate, reqdate, notes, discount,
                         taxincluded, company, contact, email, address, shiptoaddress)

    def orderItemValues(self):
        return self.objectValues('BLOrderItem')

    def isBuy(self):
        """
        returns true if this a purchase order
        """
        return self.blLedger().isPayable()

    def isSell(self):
        """
        return true if this is a sales order
        """
        return not self.isBuy()

    def manage_edit(self, title, orderdate, reqdate, notes='', discount=0.0, taxincluded=0, company='',
	            contact='', email='', billingaddress='', shiptoaddress='', items=[], REQUEST=None):
        """ """
        if not isinstance(orderdate, DateTime):
            orderdate = DateTime(orderdate)
        if not isinstance(reqdate, DateTime):
            reqdate = DateTime(reqdate)
        try:
            discount = float(discount)
        except:
            raise ValueError, discount
        if discount > 100.0 or discount < 0.0:
            raise ValueError, discount
	self.title = title
        self.orderdate = orderdate
        self.reqdate = reqdate
        self.taxincluded = bool(taxincluded)
        self.discount = discount / 100.0
        self.notes = notes
        self.contact = contact
        self.email = email
        self.company = company
        self.billingaddress = billingaddress
        self.shiptoaddress = shiptoaddress
        self.entered_by = getSecurityManager().getUser().getUserName()
        if items:
            for item in items:
                ob = self._getOb(item.id)
                ob.quantity = item.qty
                try:
                    assert_currency(item.unit)
                    ob.unit = item.unit
                except:
                    ob.unit = ZCurrency(item.unit)
            # if we're called from ctor (without context) we fail ...
            try:
                self.setStatus()
            except AttributeError:
                pass

        if REQUEST is not None:
            return self.manage_main(self, REQUEST)

    def contactEmail(self):
        """
        the email address of the contact person
        """
        return self.email

    def companyName(self):
        """
        """
        return self.company

    def billingAddress(self):
        """
        """
        return self.billingaddress

    def shippingAddress(self):
        """
        """
        return self.shiptoaddress

    def manage_delObjects(self, ids=[], REQUEST=None):
        """
        delete order items and possibly demote status
        """
        if ids:
            PortalFolder.manage_delObjects(self, ids)
            self.setStatus()
        if REQUEST:
            return self.manage_main(self, REQUEST)

    def notifyWorkflowCreated(self):
        # hmmm CMFCatalogAware causes our workflow to hang as our first
        # transition is AUTOMATIC ...
        pass

    def setStatus(self):
        """
        does automatic status promotions - as all these functions are private :(
        """
        wftool = getToolByName(self, 'portal_workflow')
        status = self.status()

        if status == 'incomplete':
	    # some orders may contain 'free' items ...
            if self.getGross() > 0 or self.orderItemValues():
                #raise AssertionError, 'doing open transition...'
                wf = wftool.getWorkflowsFor(self)[0]
                wf._executeTransition(self, wf.transitions.ordering)
        elif status == 'open':
            if not self.orderItemValues():
                wf = wftool.getWorkflowsFor(self)[0]
                wf._executeTransition(self, wf.transitions.incomplete)
	elif status == 'paid':
	    # if its a zero-value order then promote payment to confirmed
	    txn = self.blTransaction()
	    if not txn or txn.creditTotal() == 0:
		wf._executeTransition(self, wf.transitions.confirm)
        self.indexObject(idxs=['status'])

    def all_meta_types(self):
        """
        only allow adding Order Items if status is open, uninvoiced, incomplete
        """
        if self.status() in ('open', 'uninvoiced', 'incomplete'):
            return [ ProductsDictionary('BLOrderItem') ]
        return []

    def getDiscount(self):
        """
        """
        return self.getGross() * self.discount

    def getGross(self):
        """
        return total value of order before deductions
        """
        # this is written to allow for non-commutitivity of ZCurrency ...
        items = self.objectValues('BLOrderItem')

        if items:
            return reduce(operator.add, map(lambda x: x.calculateGrossPrice(), items))

        return self.blAccount().zeroAmount()

    def getTax(self):
        """
        return the total tax(s) levied on this order
        """
        tax = self.blAccount().zeroAmount()
        if not self.taxincluded:
            sales_tax = self.isReceivable() and 'sales_tax_paid' or 'sales_tax_due'
            tax_accts = self.Ledger.accountValues(tags=sales_tax)
            if tax_accts:
                tax = reduce(operator.add,
                             map(lambda x: self.calculateTax(x, self.orderdate), tax_accts))
        return tax

    def getNet(self, **kw):
        """
        return net value (plus tax, freight, less discounts etc) of order
        """
        #
        # we are assuming tax rates apply evenly across all items ...
        # this could delegate to calculateTax, but it's more efficient!
        #
        effective = kw.get('effective', self.orderdate)
        total = self.getGross()
            
        if self.discount:
            total = total * (1.0 - self.discount)

	# add freight charges  -  quietly ignore if blorder_frieght script not found or broken ...
        try:
            freight = self.blorder_freight()
        except:
            freight = 0
            
	if freight > 0:
	    if freight.currency() != total.currency():
	        rate = self.portal_bastionledger.crossBuyRate(total.currency(), freight.currency(), effective)
	        freight = ZCurrency(total.currency(), freight.amount() / rate)
	    total += freight

        # calculate tax on amount + freight
        if not self.taxincluded:
            bt = getToolByName(self, 'portal_bastionledger')
            tax = bt.calculateTax(effective, total, self.aq_parent)
            total += tax

	return total

    def modifiable(self):
        return  self.status() in ('open', 'incomplete',)

    def invoiceDate(self):
	"""
	return the date the order was posted to the ledger
	"""
	txn = self.blTransaction()
	if txn:
	    return txn.effective()
        # hmmm - not posted yet - let's just return None
        return None

    def modificationTime(self):
        """ """
        return self.bobobase_modification_time().strftime('%Y-%m-%d %H:%M')

    def weight(self):
	"""
	return the weight of the order - used to calculate freigh costs etc
	"""
	weight = 0.0
	for item in self.orderItemValues():
	    weight += item.blPart().weight * item.quantity
	return weight

    def calculateTax(self, tax_account=None, effective=None):
        """
        this is to display total tax for a single tax rate ...
        if no tax account is given, use the underlying ledger account
        """
        # TODO - we need to be checking tax codes in the account and tiering
        # against this ...
        tool = getToolByName(self, 'portal_bastionledger')

        # we just need the tax codes on the underlying account ...
        return tool.calculateTax(effective or self.orderdate,
                                 self.getGross() * (1.0 - self.discount),
                                 self.blAccount())
        
    def manage_cancel(self, REQUEST=None):
        """
	cancel the order, reversing any transaction  
	"""
	if not self.status() == 'cancelled':
	    status = 'cancelled'
	    txn = self.blTransaction()
            # maybe some dickhead reversed the txn separate to cancelling the order ...
            if txn:
                if txn.status() == 'posted':
                    txn.manage_reverse()
                elif txn.status() != 'reversed':
                    raise PostingError, 'Somebody has screwed with the transaction underlying this order'
            self._status(status)
	
        if REQUEST is not None:
            return self.manage_main(self, REQUEST)
            
    def manage_invoice(self, REQUEST=None):
        """
        post an invoice transaction ...
        """
	if not self.status() == 'invoiced':
            self._status('invoiced')
            self.aq_parent.manage_postOrder(self)
                          
        if REQUEST:
            return self.manage_main(self, REQUEST)

    def manage_finish(self, REQUEST=None):
        """
        post an invoice transaction ...
        """
	if not self.status() == 'processed':
            self._status('processed')
                          
        if REQUEST:
            return self.manage_main(self, REQUEST)

    def blTransaction(self):
	"""
	if this order has been posted at any time in it's workflow, it will
	have a txn_id

	Note that if it's a zero-value order, then there will be no transaction...
	"""
	if hasattr(aq_base(self), 'txn_id'):
	    return self.blLedger()._getOb(self.txn_id, None)
	return None

    def blAccount(self):
        """
        return the underlying ledger account
        """
        # hmmm - overloading functionality for different contexts
        accid = getattr(aq_base(self), '_account', None)
        if accid:
            try:
                return getattr(aq_base(self.aq_parent), accid)
            except AttributeError:
                pass

        return self.aq_parent
            
    def upgradeToAccount(self, orderbook_id, accno=''):
        """
        take the order details and create an orderaccount in the given orderbook
        """
        orderbook = self.bastionLedger()._getOb(orderbook_id)
        assert isinstance(orderbook, BLOrderBook), 'Not a BLOrderBook: %s' % orderbook

        aid = manage_addBLOrderAccount(orderbook, id=accno,
                                       title=self.company or self.contact, 
                                       currency=self.getGross().currency(), 
                                       taxincluded=self.taxincluded,
                                       discount=self.discount,
                                       email=self.email, 
                                       notes=self.notes,
                                       address=self.address, 
                                       shiptoaddress=self.shiptoaddress,
                                       shiptophone=self.shiptophone,
                                       shiptofax=self.shiptofax,
                                       shiptoemail=self.shiptoemail)
                                       
        return orderbook._getOb(aid)

    def manage_emailInvoice(self, notify_email='', message='',REQUEST=None):
        """
        email invoice to recipient ...
        """
        try:
            mailhost = self.superValues(['Mail Host', 'Secure Mail Host'])[0]
        except:
            # TODO - exception handling ...
            if REQUEST:
                REQUEST.set('manage_tabs_message', 'No Mail Host Found')
                return self.manage_main(self, REQUEST)
            raise
        
        if not notify_email:
            notify_email = self.blAccount().email

        sender = self.blLedger().email
        if not sender:
            if REQUEST:
                REQUEST.set('manage_tabs_message', """Ledger's Correpondence Email unset!""")
                return self.manage_main(self, REQUEST)
            raise LedgerError, """Ledger's Correspondence Email unset!"""

        # ensure 7-bit
        mail_text = str(self.blinvoice_template(self, self.REQUEST, email=notify_email))

        mailhost._send(sender, notify_email, mail_text)

        if REQUEST:
            REQUEST.set('manage_tabs_message', 'Invoice emailed to %s' % notify_email)
            return self.manage_main(self, REQUEST)

    def blInventory(self):
        """
        the parts repository for this orderbook
        """
        ledger = self.blLedger()
        #while not ledger.meta_type in ('BLOrderBook', 'BLCashBook'):
        #    ledger = ledger.aq_parent

        return ledger.aq_parent._getOb(ledger.inventory, None)
    
    def availableParts(self):
        """
        the parts that can be ordered
        """
        #
        # TODO: restrict this to only onhand ???
        #
        return map(lambda x: x.getObject(),
                   self.blInventory().catalog(meta_type='BLPart'))

    def _setObject(self, id, object, roles=None, user=None, set_owner=1):
        PortalFolder._setObject(self, id, object, roles, user, set_owner)
        self.setStatus()

    def indexObject(self, idxs=[]):
        """ Handle indexing """
        try:
            #
            # just making sure we can use this class in a non-catalog aware situation...
            #
            cat = getattr(self, 'catalog')
            url = '/'.join(self.getPhysicalPath())
            cat.catalog_object(self, url, idxs)
        except:
            pass
        
    def unindexObject(self):
        """ Handle unindexing """
        try:
            #
            # just making sure we can use this class in a non-catalog aware situation...
            #
            cat = getattr(self, 'catalog')
            url = '/'.join(self.getPhysicalPath())
            cat.uncatalog_object(url)
        except:
            pass

    def isOpen(self):
        """ is the order still active """
        return self.status() in ['incomplete', 'open']

    def manage_status_modify(self, workflow_action, REQUEST=None):
        """
        perform the workflow (very Plone ...)
        """
        state = self.content_status_modify(workflow_action=workflow_action)
        if REQUEST:
            REQUEST.set('manage_tabs_message', 'State Changed')
            return self.manage_main(self, REQUEST)

    def manage_addOrderItem(self, part, qty=1, unit=0, discount=None, amount=None, REQUEST=None):
        """
        part is an id (or a part)
        """
        id = manage_addBLOrderItem(self, id, qty=qty, unit=unit,discount=discount, amount=amount)
        return id

    def _repair(self):
	if self.__dict__.has_key('status'):
	    status = self.__dict__['status'].lower()
	    if status == 'uninvoiced':
		status = 'open'
	    assert status != 'uninvoiced', status
	    self._status(status)
	    del self.__dict__['status']
	    self._p_changed = 1
        if self.status() == 'uninvoiced':
	    self._status('open')
	if type(self.shiptoaddress) == type([]):
	    self.shiptoaddress = '\n'.join(self.shiptoaddress)
        map( lambda x:x._repair(), self.objectValues('BLOrderItem') )


AccessControl.class_init.InitializeClass(BLOrder)

def addBLCashBook(self, id, title='',REQUEST=None):
    """
    Plone-based entry
    """
    inventory_id = self.bastionLedger().objectIds('BLInventory')[0]

    # lets just guess at the regular bank account ...
    try:
        control = self.Ledger.accountValues(tags='bank_account')[0].getId()
    except:
        control = addBLAccount(self.Ledger, id, title)
    return self.manage_addBLCashBook(id, control, inventory_id, 'C', title)


manage_addBLCashBookForm = PageTemplateFile('zpt/add_cashbook', globals()) 
def manage_addBLCashBook(self, id, controlAccount, inventory,
                         prefix='C', title='', REQUEST=None,):
    """ adds an orderbook """
    #if not self.BastionMerchantService:
    #    msg = '''You need a BastionBanking.BastionMerchantService in order to process cash orders.  This is available from http://www.last-bastion.net/BastionBanking.'''
    #    raise AttributeError, msg

    # do some checking ...
    Ledger = self.Ledger
    control = Ledger._getOb(controlAccount)
    assert control.meta_type =='BLAccount', "Incorrect Control Account Type - Must be strictly GL"
    # seems portal_factory is creating this already ...
    #assert not getattr(aq_base(control), id, None),"A Subsidiary Ledger Already Exists on %s." % control.prettyTitle()
    
    self._setObject(id, BLCashBook(id, title, control, inventory,
                                   Ledger.currencies or [self.currency], account_prefix=prefix))

    if REQUEST is not None:
        return self.manage_main(self, REQUEST)

    return id

class BLCashAccount(BLOrderAccount):
    """
    An account where all orders must be paid in advance
    This is a placeholder for polymorphically deciding upon order construction
    """
    meta_type = portal_type = 'BLCashAccount'

    def manage_addOrder(self, title='', orderdate=None, taxincluded=False, discount=0.0, 
                        buysell='buy', REQUEST=None):
        """
        Add an order to the account
        """
        id = self.aq_parent.nextOrderNo()
        self._setObject(id, 
                        BLCashOrder(id, title, 
                                    orderdate=orderdate or DateTime(),
                                    discount=discount,
                                    taxincluded=taxincluded, 
                                    buysell=buysell))
        if REQUEST:
            return REQUEST.RESPONSE.redirect('%s/%s/manage_workspace' % (self.getId(),id) )
        return self._getOb(id)

AccessControl.class_init.InitializeClass(BLCashAccount)

class BLCashOrder(BLOrder):
    """
    An order which doesn't warrant setting up an account.  You may elect to
    perform immediate cash payment too.
    """
    meta_type = portal_type = 'BLCashOrder'

    __allow_access_to_unprotected_subobjects__ = 1
    
    __ac_permissions__ = BLOrder.__ac_permissions__ + (
	(OperateBastionLedgers, ('manage_pay', 'upgradeToAccount',)),
	(OverseeBastionLedgers, ('manage_confirm',)),
	)

    BuySell = ('buy', 'sell')

    buysell = 'buy'

    _properties = BLOrder._properties + (
        {'id':'buysell', 'type':'selection', 'mode':'w', 'select variable':'BuySell'},
        )

    def __init__(self, id, title, discount=0.0, taxincluded=0, notes='', shiptoaddress='',
                 contact='', email='', orderdate=DateTime(), reqdate=DateTime() + 7, buysell='sell'):
        BLOrder.__init__(self, id, title, discount, taxincluded, notes, shiptoaddress,
                         contact, email, orderdate, reqdate)
        self._updateProperty('buysell', buysell)

    def isBuy(self):
        """
        locally decide if we're a buy or sell order
        """
        return self.buysell == 'buy'

    def manage_pay(self, REQUEST=None):
        """
        post the payment against the account (and implicitly the bank account)
        """
        self.manage_postOrder(self)

        if REQUEST:
            return self.manage_main(self, REQUEST)

    #
    # TODO - think about permissions on this - we're kind of expecting a third-party
    # back-end to trigger this - they won't have permissions ...
    #

    def manage_confirm(self, stateobject=None, REQUEST=None):
        """
        confirm that payment has been received - this should be called from
        an after transition, otherwise further processing will stomp over the
        state we're setting
        """
        txn = self.blTransaction()

	# if it's a zero-value order, there will be no txn ...
	if not txn:
	   return

        tstatus = txn.status()

        if tstatus == 'posted':
            # excellent - as you were ...
            return

        #
        # OK - we're going to screw with the workflow engine and return
        # to a prior state without running any transitions (hopefully)
        #
        wftool = getToolByName(self, 'portal_workflow')
        wf = wftool.getWorkflowsFor(self)[0]
        if tstatus == 'complete':
            # still no word from payment service - remain in confirm state (maybe
            # we could promote this to dispatched ...)
            states = stateob.old_state
        elif tstatus == 'rejected':
            states = stateob.new_state
            states['review_state'] = 'open'
        else:
            raise InvalidState, 'confirm received bad transaction state: %s (tid=%s)' % (tstatus, txn.getId())  
        wf._changeStateOf(txn, states)


AccessControl.class_init.InitializeClass(BLCashOrder)

class BLCashBook(BLOrderBook):
    """
    This is a repository for cash orders.
    
    The the posting and paying for an order are combined.
    """
    meta_type = portal_type = 'BLCashBook'

    account_types = ('BLCashAccount',)

    implements(ICashBook)

    def __init__(self, id, title, control, inventory, currencies,
                 order_prefix='Q', account_prefix='C', txn_prefix='C' ):
        BLOrderBook.__init__(self, id, title, control, inventory, currencies,
                             order_prefix, account_prefix, txn_prefix )

    def nextAccountId(self):
        raise AssertionError, 'This should be irrelevant!'

    def _delObject(self, id, tp=1, suppress_events=False):
        if id == 'CASH':
            raise BeforeDeleteException, 'Cannot delete CASH account'
        BLCashBook._delObject(id, tp,suppress_events)

    def orderStatuses(self):
        """
        return a list of status ids associated with order workflow
        """
        return getToolByName(self, 'portal_workflow').blcashorder_workflow.states.objectIds()

    def _repair(self):
        #if not getattr(aq_base(self.Processes), 'postOrder', None):
        #    self.isreceivable = None
        #    self._updateProperty('isreceivable', self.controlAccount().type == 'Asset')
        BLOrderBook._repair(self)

AccessControl.class_init.InitializeClass(BLCashBook)


def addOrder(ob, event):
    parent = ob.aq_parent
    
    for attr in (('contact', ('shiptoname', 'contact',)),
                 ('email', ('shiptoemail', 'email',)),
                 ('company', ('title',)),
                 ('taxincluded', ('taxincluded',)),
                 ('billingaddress', ('address',)),
                 ('shiptoaddress', ('shiptoaddress', 'address')),
                 ('shiptophone', ('shiptophone', 'phone')), 
                 ('shiptofax', ('shiptofax', 'fax')),):
        if ob.getProperty(attr[0], '') == '':
            for override in attr[1]:
                value = parent.getProperty(override, '')
                if value != '':
                    ob._updateProperty(attr[0], value)

    # hmmm - acquire notes - if it's a QuoteManager, the field is called 'disclaimer'
    if isinstance(parent, BLOrderAccount):
        notes = parent.notes or parent.aq_parent.instructions
    else:
        notes = getattr(parent, 'disclaimer', '')
    if notes:
        ob._updateProperty('notes', notes)

    ob.indexObject()
    # force auto state change ...
    ob.setStatus()

def addCashBook(ob, event):
    # TODO - cash books should be ambiguous but we're making them Asset's ...

    # if it's a copy, then this will fail because we don't allow delete ...
    if not getattr(aq_base(ob), 'CASH', None):
        ob._setObject('CASH',
                      BLCashAccount('CASH', 'Cash Account', '', 'Asset',
                                    ob.defaultCurrency()))

    bt = getToolByName(ob, 'portal_bastionledger')
    if bt.hasTaxTable('sales_tax'):
        account = ob.CASH
        account.manage_addTaxGroup('sales_tax')

