# # 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')