#############################################################################
#
# Copyright (c) 2004 Nexedi SARL and Contributors. All Rights Reserved.
#          Jerome Perrin <jerome@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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.
#
##############################################################################

"""
  Tests some functionnalities of accounting.

"""
import os, sys
if __name__ == '__main__':
  execfile(os.path.join(sys.path[0], 'framework.py'))

# Needed in order to have a log file inside the current folder
os.environ['EVENT_LOG_FILE'] = os.path.join(os.getcwd(), 'zLOG.log')
os.environ['EVENT_LOG_SEVERITY'] = '-300'

from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from AccessControl.SecurityManagement import newSecurityManager
from zLOG import LOG
from Products.ERP5Type.tests.Sequence import Sequence, SequenceList
from DateTime import DateTime

SOURCE = 'source'
DESTINATION = 'destination'
RUN_ALL_TESTS = 1
QUIET = 1

class TestAccounting(ERP5TypeTestCase):
  """Test Accounting. """
  
  def getAccountingModule(self):
    return getattr(self.getPortal(), 'accounting_module',
           getattr(self.getPortal(), 'accounting', None))
  
  def getAccountModule(self) :
    return getattr(self.getPortal(), 'account_module',
           getattr(self.getPortal(), 'account', None))
  
  # XXX
  def playSequence(self, sequence_string, quiet=1) :
    sequence_list = SequenceList()
    sequence_list.addSequenceString(sequence_string)
    sequence_list.play(self, quiet=quiet)
  
  account_portal_type           = 'Account'
  accounting_period_portal_type = 'Accounting Period'
  accounting_transaction_portal_type = 'Accounting Transaction'
  accounting_transaction_line_portal_type = 'Accounting Transaction Line'
  currency_portal_type          = 'Currency'
  organisation_portal_type      = 'Organisation'
  sale_invoice_portal_type      = 'Sale Invoice Transaction'
  sale_invoice_line_portal_type = 'Sale Invoice Line' 
  sale_invoice_transaction_line_portal_type = 'Sale Invoice Transaction Line'
  sale_invoice_cell_portal_type = 'Invoice Cell'
  purchase_invoice_portal_type      = 'Purchase Invoice Transaction'
  purchase_invoice_line_portal_type = 'Purchase Invoice Line' 
  purchase_invoice_transaction_line_portal_type = \
                'Purchase Invoice Transaction Line'
  purchase_invoice_cell_portal_type = 'Invoice Cell'

  start_date = DateTime(2004, 01, 01)
  stop_date  = DateTime(2004, 12, 31)

  default_region = 'europe/west/france'

  def getTitle(self):
    return "Accounting"
  
  def afterSetUp(self):
    """Prepare the test."""
    self.createCategories()
    self.login()

  def login(self) :
    """sets the security manager"""
    uf = self.getPortal().acl_users
    uf._doAddUser('alex', '', ['Member', 'Assignee', 'Assignor',
                               'Auditor', 'Author', 'Manager'], [])
    user = uf.getUserById('alex').__of__(uf)
    newSecurityManager(None, user)
  
  def createCategories(self):
    """Create the categories for our test. """
    # create categories
    for cat_string in self.getNeededCategoryList():
      base_cat = cat_string.split("/")[0]
      path = self.getPortal().portal_categories[base_cat]
      for cat in cat_string.split("/")[1:]:
        if not cat in path.objectIds():
          path = path.newContent(
            portal_type='Category',
            id=cat,
            immediate_reindex=1)
        else:
          path = path[cat]
          
    # check categories have been created
    for cat_string in self.getNeededCategoryList() :
      self.assertNotEquals(None,
                self.getCategoryTool().restrictedTraverse(cat_string),
                cat_string)
                
  def getNeededCategoryList(self):
    """Returns a list of categories that should be created."""
    return ('group/client', 'group/vendor',
            'region/%s'%self.default_region, )
  
  def getBusinessTemplateList(self):
    """Returns list of BT to be installed."""
    return ('erp5_base', 'erp5_pdm', 'erp5_trade', 'erp5_accounting',)

  def stepTic(self, **kw):
    """Flush activity queue. """
    self.tic()

  def stepCreateEntities(self, sequence, **kw) :
    """Create a vendor and a client. """
    client = self.getOrganisationModule().newContent(
        portal_type = self.organisation_portal_type,
        group = "group/client",
        price_currency = "currency_module/USD")
    vendor = self.getOrganisationModule().newContent(
        portal_type = self.organisation_portal_type,
        group = "group/vendor",
        price_currency = "currency_module/EUR")
    # validate entities
    for entity in (client, vendor):
      entity.setRegion(self.default_region)
      self.getWorkflowTool().doActionFor(entity, 'validate_action')
    
    sequence.edit( client = client,
                   vendor = vendor,
                   organisation = vendor )
  
  def stepCreateAccountingPeriod(self, sequence, **kw):
    """Creates an Accounting Period for the Organisation."""
    organisation = sequence.get('organisation')
    start_date = self.start_date
    stop_date = self.stop_date
    accounting_period = organisation.newContent(
      portal_type = self.accounting_period_portal_type,
      start_date = start_date, stop_date = stop_date )
    sequence.edit( accounting_period = accounting_period,
                   valid_date_list = [ start_date, start_date+1, stop_date],
                   invalid_date_list = [start_date-1, stop_date+1] )
    
  def stepUseValidDates(self, sequence, **kw):
    """Puts some valid dates in sequence."""
    sequence.edit(date_list = sequence.get('valid_date_list'))
    
  def stepUseInvalidDates(self, sequence, **kw):
    """Puts some invalid dates in sequence."""
    sequence.edit(date_list = sequence.get('invalid_date_list'))
  
  def stepOpenAccountingPeriod(self, sequence, **kw):
    """Opens the Accounting Period."""
    accounting_period = sequence.get('accounting_period')
    self.getPortal().portal_workflow.doActionFor(
                        accounting_period,
                        'plan_action' )
    self.assertEquals(accounting_period.getSimulationState(),
                      'planned')
                      
  def stepConfirmAccountingPeriod(self, sequence, **kw):
    """Confirm the Accounting Period."""
    accounting_period = sequence.get('accounting_period')
    self.getPortal().portal_workflow.doActionFor(
                        accounting_period,
                        'confirm_action' )
    self.assertEquals(accounting_period.getSimulationState(),
                      'confirmed')

  def stepCheckAccountingPeriodRefusesClosing(self, sequence, **kw):
    """Checks the Accounting Period refuses closing."""
    accounting_period = sequence.get('accounting_period')
    self.assertRaises(ValidationFailed,
          self.getPortal().portal_workflow.doActionFor,
          accounting_period, 'confirm_action' )

  def stepDeliverAccountingPeriod(self, sequence, **kw):
    """Deliver the Accounting Period."""
    accounting_period = sequence.get('accounting_period')
    self.getPortal().portal_workflow.doActionFor(
                        accounting_period,
                        'close_action' )
    self.assertEquals(accounting_period.getSimulationState(),
                      'closing')
    
  def stepCheckAccountingPeriodDelivered(self, sequence, **kw):
    """Check the Accounting Period is delivered."""
    accounting_period = sequence.get('accounting_period')
    self.assertEquals(accounting_period.getSimulationState(),
                      'delivered')
    
  
  def stepCreateCurrencies(self, sequence, **kw) :
    """Create a some currencies. """
    if hasattr(self.getCurrencyModule(), 'EUR'):
      sequence.edit(
        EUR = self.getCurrencyModule()['EUR'],
        USD = self.getCurrencyModule()['USD'],
        YEN = self.getCurrencyModule()['YEN'],
      )
      return
    EUR = self.getCurrencyModule().newContent(
          portal_type = self.currency_portal_type,
          reference = "EUR",
          id = "EUR" )
    USD = self.getCurrencyModule().newContent(
          portal_type = self.currency_portal_type,
          reference = "USD",
          id = "USD" )
    YEN = self.getCurrencyModule().newContent(
          portal_type = self.currency_portal_type,
          reference = "YEN",
          id = "YEN" )
    sequence.edit( EUR = EUR, USD = USD, YEN = YEN )
  
  def stepCreateAccounts(self, sequence, **kw) :
    """Create necessary accounts. """
    receivable = self.getAccountModule().newContent(
          title = 'receivable',
          portal_type = self.account_portal_type,
          account_type = 'asset/receivable' )
    payable = self.getAccountModule().newContent(
          title = 'payable',
          portal_type = self.account_portal_type,
          account_type = 'liability/payable' )
    expense = self.getAccountModule().newContent(
          title = 'expense',
          portal_type = self.account_portal_type,
          account_type = 'expense' )
    income = self.getAccountModule().newContent(
          title = 'income',
          portal_type = self.account_portal_type,
          account_type = 'income' )
    collected_vat = self.getAccountModule().newContent(
          title = 'collected_vat',
          portal_type = self.account_portal_type,
          account_type = 'liability/payable/collected_vat' )
    refundable_vat = self.getAccountModule().newContent(
          title = 'refundable_vat',
          portal_type = self.account_portal_type,
          account_type = 'asset/receivable/refundable_vat' )
    bank = self.getAccountModule().newContent(
          title = 'bank',
          portal_type = self.account_portal_type,
          account_type = 'asset/cash/bank')
    
    # set mirror accounts.
    receivable.setDestinationValue(payable)
    payable.setDestinationValue(receivable)
    expense.setDestinationValue(income)
    income.setDestinationValue(expense)
    collected_vat.setDestinationValue(refundable_vat)
    refundable_vat.setDestinationValue(collected_vat)
    bank.setDestinationValue(bank)
    
    account_list = [ receivable,
                     payable,
                     expense,
                     income,
                     collected_vat,
                     refundable_vat,
                     bank ]

    for account in account_list :
      account.validate()
      self.failUnless('Site Error' not in account.view())
      self.assertEquals(account.getValidationState(), 'validated')
      
    sequence.edit( receivable_account = receivable,
                   payable_account = payable,
                   expense_account = expense,
                   income_account = income,
                   collected_vat_account = collected_vat,
                   refundable_vat_account = refundable_vat,
                   bank_account = bank,
                   account_list = account_list )

  
  def stepCreateAccountingTransactionAndCheckMirrorAccount(self,
                                          sequence, **kw):
    """Check that mirror account are set automatically. """
    account_list = sequence.get('account_list')
    
    for account in account_list :
      self.assertNotEquals(account.getDestinationValue(), None)
    
    transaction = self.getAccountingModule().newContent(
      portal_type = self.accounting_transaction_portal_type,
      source_section_value = sequence.get('client'),
      resource_value = sequence.get('EUR'),
      created_by_builder = 1,
    )
    
    # setting both source and destination shouldn't use mirror accounts
    destination = sequence.get('receivable_account')
    for account in account_list :
      transaction_line = transaction.newContent(
        portal_type = self.accounting_transaction_line_portal_type,
        source = account.getRelativeUrl(),
        destination = destination.getRelativeUrl(),
      )
      self.assertEquals( destination.getRelativeUrl(),
                         transaction_line.getDestination() )
    
    # setting only a source must use mirror account as destination
    for account in account_list :
      transaction_line = transaction.newContent(
        portal_type = self.accounting_transaction_line_portal_type,
        source = account.getRelativeUrl(),
      )
      self.assertEquals( account.getDestination(),
                         transaction_line.getDestination() )
    
    # editing the destination later should not change the source once
    # the mirror account has been set.
    account = sequence.get('receivable_account')
    destination = sequence.get('bank_account')
    another_destination = sequence.get('expense_account')
    account.setDestinationValueList(account_list)
    
    transaction_line = transaction.newContent(
      portal_type = self.accounting_transaction_line_portal_type,
      source = account.getRelativeUrl(), )
    automatically_set_destination = transaction_line.getDestinationValue()
    # get another account.
    if automatically_set_destination == destination :
      forced_destination = destination
    else :
      forced_destination = another_destination
    # set all other accounts as mirror account to this one.
    forced_destination.setDestinationValueList(account_list)
    
    # change the destination and check the source didn't change.
    transaction_line.edit(destination = forced_destination.getRelativeUrl())
    self.assertEquals( transaction_line.getSourceValue(), account )
    
  def getInvoicePropertyList(self):
    """Returns the list of properties for invoices, stored as 
      a list of dictionnaries. """
    # source currency is EUR
    # destination currency is USD
    return [
      # in currency of destination, converted for source
      { 'income' : -200,             'source_converted_income' : -180,
        'collected_vat' : -40,       'source_converted_collected_vat' : -36,
        'receivable' : 240,          'source_converted_receivable' : 216,
        'currency' : 'currency_module/USD' },
      
      # in currency of source, converted for destination
      { 'income' : -100,        'destination_converted_expense' : -200,
        'collected_vat' : 10,   'destination_converted_refundable_vat' : 100,
        'receivable' : 90,      'destination_converted_payable' : 100,
        'currency' : 'currency_module/EUR' },
      
      { 'income' : -100,        'destination_converted_expense' : -200,
        'collected_vat' : 10,   'destination_converted_refundable_vat' : 100,
        'receivable' : 90,      'destination_converted_payable' : 100,
        'currency' : 'currency_module/EUR' },
      
      # in an external currency, converted for both source and dest.
      { 'income' : -300,
                    'source_converted_income' : -200,
                    'destination_converted_expense' : -400,
        'collected_vat' : 40,
                    'source_converted_collected_vat' : 36,
                    'destination_converted_refundable_vat' : 50,
        'receivable' : 260,
                    'source_converted_receivable' : 164,
                    'destination_converted_payable': 350,
        'currency' : 'currency_module/YEN' },
      
      # currency of source, not converted for destination -> 0
      { 'income' : -100,
        'collected_vat' : -20,
        'receivable' : 120,
        'currency' : 'currency_module/EUR' },
      
    ]
  
  def stepCreateInvoices(self, sequence, **kw) :
    """Create invoices with properties from getInvoicePropertyList. """
    invoice_prop_list = self.getInvoicePropertyList()
    invoice_list = []
    date_list = sequence.get('date_list')
    if not date_list : date_list = [ DateTime(2004, 12, 31) ]
    i = 0
    for invoice_prop in invoice_prop_list :
      i += 1
      date = date_list[i % len(date_list)]
      invoice = self.getAccountingModule().newContent(
          portal_type = self.sale_invoice_portal_type,
          source_section_value = sequence.get('vendor'),
          source_value = sequence.get('vendor'),
          destination_section_value = sequence.get('client'),
          destination_value = sequence.get('client'),
          resource = invoice_prop['currency'],
          start_date = date, stop_date = date,
          created_by_builder = 0,
      )
      
      for line_type in ['income', 'receivable', 'collected_vat'] :
        source_account = sequence.get('%s_account' % line_type)
        line = invoice.newContent(
          portal_type = self.sale_invoice_transaction_line_portal_type,
          quantity = invoice_prop[line_type],
          source_value = source_account
        )
        source_converted = invoice_prop.get(
                          'source_converted_%s' % line_type, None)
        if source_converted is not None :
          line.setSourceTotalAssetPrice(source_converted)
        
        destination_account = source_account.getDestinationValue(
                                                portal_type = 'Account' )
        destination_converted = invoice_prop.get(
                          'destination_converted_%s' %
                          destination_account.getAccountTypeId(), None)
        if destination_converted is not None :
          line.setDestinationTotalAssetPrice(destination_converted)
 
      invoice_list.append(invoice)
    sequence.edit( invoice_list = invoice_list )
  
  def stepCreateOtherSectionInvoices(self, sequence, **kw):
    """Create invoice for other sections."""
    other_source = self.getOrganisationModule().newContent(
                      portal_type = 'Organisation' )
    other_destination = self.getOrganisationModule().newContent(
                      portal_type = 'Organisation' )
    invoice = self.getAccountingModule().newContent(
        portal_type = self.sale_invoice_portal_type,
        source_section_value = other_source,
        source_value = other_source,
        destination_section_value = other_destination,
        destination_value = other_destination,
        resource_value = sequence.get('EUR'),
        start_date = self.start_date,
        stop_date = self.start_date,
        created_by_builder = 0,
    )
    
    line = invoice.newContent(
        portal_type = self.sale_invoice_transaction_line_portal_type,
        quantity = 100, source_value = sequence.get('account_list')[0])
    line = invoice.newContent(
        portal_type = self.sale_invoice_transaction_line_portal_type,
        quantity = -100, source_value = sequence.get('account_list')[1])
    sequence.edit(invoice_list = [invoice])
  
  def stepStopInvoices(self, sequence, **kw) :
    """Validates invoices."""
    invoice_list = sequence.get('invoice_list')
    for invoice in invoice_list:
      self.getPortal().portal_workflow.doActionFor(
          invoice, 'stop_action')
  
  def stepCheckStopInvoicesRefused(self, sequence, **kw) :
    """Checks that invoices cannot be validated."""
    invoice_list = sequence.get('invoice_list')
    for invoice in invoice_list:
      self.assertRaises(ValidationFailed,
          self.getPortal().portal_workflow.doActionFor,
          invoice, 'stop_action')

  def stepCheckInvoicesAreDraft(self, sequence, **kw) :
    """Checks invoices are in draft state."""
    invoice_list = sequence.get('invoice_list')
    for invoice in invoice_list:
      self.assertEquals(invoice.getSimulationState(), 'draft')

  def stepCheckInvoicesAreStopped(self, sequence, **kw) :
    """Checks invoices are in stopped state."""
    invoice_list = sequence.get('invoice_list')
    for invoice in invoice_list:
      self.assertEquals(invoice.getSimulationState(), 'stopped')
      
  def stepCheckInvoicesAreDelivered(self, sequence, **kw) :
    """Checks invoices are in delivered state."""
    invoice_list = sequence.get('invoice_list')
    for invoice in invoice_list:
      self.assertEquals(invoice.getSimulationState(), 'delivered')
      
  def checkAccountBalanceInCurrency(self, section, currency,
                                          sequence, **kw) :
    """ Checks accounts balances in a given currency."""
    invoice_list = sequence.get('invoice_list')
    for account_type in [ 'income', 'receivable', 'collected_vat',
                          'expense', 'payable', 'refundable_vat' ] :
      account = sequence.get('%s_account' % account_type)
      calculated_balance = 0
      for invoice in invoice_list :
        for line in invoice.getMovementList():
          # source
          if line.getSourceValue() == account and\
             line.getResourceValue() == currency and\
             section == line.getSourceSectionValue() :
            calculated_balance += (
                    line.getSourceDebit() - line.getSourceCredit())
          # dest.
          elif line.getDestinationValue() == account and\
            line.getResourceValue() == currency and\
            section == line.getDestinationSectionValue() :
            calculated_balance += (
                    line.getDestinationDebit() - line.getDestinationCredit())
      
      self.assertEquals(calculated_balance,
          self.getPortal().portal_simulation.getInventory(
            node_uid = account.getUid(),
            section_uid = section.getUid(),
            resource_uid = currency.getUid(),
          ))
  
  def stepCheckAccountBalanceLocalCurrency(self, sequence, **kw) :
    """ Checks accounts balances in the organisation default currency."""
    for section in (sequence.get('vendor'), sequence.get('client')) :
      currency = section.getPriceCurrencyValue()
      self.checkAccountBalanceInCurrency(section, currency, sequence)
  
  def stepCheckAccountBalanceExternalCurrency(self, sequence, **kw) :
    """ Checks accounts balances in external currencies ."""
    for section in (sequence.get('vendor'), sequence.get('client')) :
      for currency in (sequence.get('USD'), sequence.get('YEN')) :
        self.checkAccountBalanceInCurrency(section, currency, sequence)
    
  def checkAccountBalanceInConvertedCurrency(self, section, sequence, **kw) :
    """ Checks accounts balances converted in section default currency."""
    invoice_list = sequence.get('invoice_list')
    for account_type in [ 'income', 'receivable', 'collected_vat',
                          'expense', 'payable', 'refundable_vat' ] :
      account = sequence.get('%s_account' % account_type)
      calculated_balance = 0
      for invoice in invoice_list :
        for line in invoice.getMovementList() :
          if line.getSourceValue() == account and \
             section == line.getSourceSectionValue() :
            calculated_balance += line.getSourceInventoriatedTotalAssetPrice()
          elif line.getDestinationValue() == account and\
               section == line.getDestinationSectionValue() :
            calculated_balance += \
                             line.getDestinationInventoriatedTotalAssetPrice()
      self.assertEquals(calculated_balance,
          self.getPortal().portal_simulation.getInventoryAssetPrice(
            node_uid = account.getUid(),
            section_uid = section.getUid(),
          ))
  
  def stepCheckAccountBalanceConvertedCurrency(self, sequence, **kw):
    """Checks accounts balances converted in the organisation default
    currency."""
    for section in (sequence.get('vendor'), sequence.get('client')) :
      self.checkAccountBalanceInConvertedCurrency(section, sequence)
  
  def stepCheckAccountingTransactionDelivered(self, sequence, **kw):
    """Checks all accounting transaction related to `organisation`
      are in delivered state. """
    organisation = sequence.get('organisation').getRelativeUrl()
    accounting_module = self.getPortal().accounting_module
    for transaction in accounting_module.objectValues() :
      if transaction.getSourceSection() == organisation \
          or transaction.getDestinationSection() == organisation :
        if self.start_date <= transaction.getStartDate() <= self.stop_date :
          self.assertEquals(transaction.getSimulationState(), 'delivered')
  
  def stepCheckAcquisition(self, sequence, **kw):
    """Checks acquisition and portal types configuration. """
    resource_value = sequence.get('EUR')
    source_section_title = "Source Section Title"
    destination_section_title = "Destination Section Title"
    source_section_value = self.getOrganisationModule().newContent(
        portal_type = self.organisation_portal_type,
        title = source_section_title,
        group = "group/client",
        price_currency = "currency_module/USD")
    destination_section_value = self.getOrganisationModule().newContent(
        portal_type = self.organisation_portal_type,
        title = destination_section_title,
        group = "group/vendor",
        price_currency = "currency_module/EUR")
    
    portal = self.getPortal()
    accounting_module = portal.accounting_module
    self.failUnless('Site Error' not in accounting_module.view())
    self.assertNotEquals(
          len(portal.getPortalAccountingMovementTypeList()), 0)
    self.assertNotEquals(
          len(portal.getPortalAccountingTransactionTypeList()), 0)
    for accounting_portal_type in portal\
                    .getPortalAccountingTransactionTypeList():
      accounting_transaction = accounting_module.newContent(
            portal_type = accounting_portal_type,
            source_section_value = source_section_value,
            destination_section_value = destination_section_value,
            resource_value = resource_value )
      self.failUnless('Site Error' not in accounting_transaction.view())
      self.assertEquals( accounting_transaction.getSourceSectionValue(),
                         source_section_value )
      self.assertEquals( accounting_transaction.getDestinationSectionValue(),
                         destination_section_value )
      self.assertEquals( accounting_transaction.getResourceValue(),
                         resource_value )
      self.assertNotEquals(
              len(accounting_transaction.allowedContentTypes()), 0)
      tested_line_portal_type = 0
      for line_portal_type in portal.getPortalAccountingMovementTypeList():
        allowed_content_types = [x.id for x in
                            accounting_transaction.allowedContentTypes()]
        if line_portal_type in allowed_content_types :
          line = accounting_transaction.newContent(
            portal_type = line_portal_type, )
          self.failUnless('Site Error' not in line.view())
          # section and resource is acquired from parent transaction.
          self.assertEquals( line.getDestinationSectionValue(),
                             destination_section_value )
          self.assertEquals( line.getDestinationSectionTitle(),
                             destination_section_title )
          self.assertEquals( line.getSourceSectionValue(),
                             source_section_value )
          self.assertEquals( line.getSourceSectionTitle(),
                             source_section_title )
          self.assertEquals( line.getResourceValue(),
                             resource_value )
          tested_line_portal_type = 1
      self.assert_(tested_line_portal_type, ("No lines tested ... " +
                          "getPortalAccountingMovementTypeList = %s " +
                          "<%s>.allowedContentTypes = %s") %
                          (portal.getPortalAccountingMovementTypeList(),
                            accounting_transaction.getPortalType(),
                            allowed_content_types ))
  
  def stepCreateValidAccountingTransaction(self, sequence,
                                          sequence_list=None, **kw) :
    """Creates a valide accounting transaction and put it in
    the sequence as `transaction` key. """
    resource_value = sequence.get('EUR')
    source_section_value = sequence.get('vendor')
    destination_section_value = sequence.get('client')
    start_date = DateTime(2000,01,01)
    stop_date = DateTime(2000,02,02)
    # get a date inside openned period, if any
    for openned_source_section_period in\
      source_section_value.searchFolder(
            portal_type =  self.accounting_period_portal_type,
            simulation_state = 'planned' ):
      start_date = openned_source_section_period.getStartDate() + 1
    for openned_destination_section_period in\
      destination_section_value.searchFolder(
            portal_type =  self.accounting_period_portal_type,
            simulation_state = 'planned' ):
      stop_date = openned_destination_section_period.getStartDate() + 1

    transaction = self.getAccountingModule().newContent(
      portal_type = self.accounting_transaction_portal_type,
      start_date = start_date,
      stop_date = stop_date,
      resource_value = resource_value,
      source_section_value = source_section_value,
      destination_section_value = destination_section_value,
      created_by_builder = 1 # XXX prevent the init script from
                             # creating lines.
    )
    income = transaction.newContent(
      portal_type = self.accounting_transaction_line_portal_type,
      quantity = 100,
      source_value = sequence.get('income_account'),
      destination_value = sequence.get('expense_account'),
    )
    self.failUnless(income.getSource() != None)
    self.failUnless(income.getDestination() != None)
    
    receivable = transaction.newContent(
      portal_type = self.accounting_transaction_line_portal_type,
      quantity = -100,
      source_value = sequence.get('receivable_account'),
      destination_value = sequence.get('payable_account'),
    )
    self.failUnless(receivable.getSource() != None)
    self.failUnless(receivable.getDestination() != None)
    
    self.failUnless(len(transaction.checkConsistency()) == 0,
      "Check consistency failed : %s" % transaction.checkConsistency())
    sequence.edit(
      transaction = transaction,
      income = income,
      receivable = receivable
    )
    
  def stepValidateNoDate(self, sequence, sequence_list=None, **kw) :
    """When no date is defined, validation should be impossible.
    
    Actually, we could say that if we have source_section, we need start_date,
    and if we have destination section, we need stop_date only, but we decided
    to update a date (of start_date / stop_date) using the other one if one is
    missing. (ie. stop_date defaults automatically to start_date if not set and
    start_date is set to stop_date in the workflow script if not set.
    """
    transaction = sequence.get('transaction')
    old_stop_date = transaction.getStopDate()
    old_start_date = transaction.getStartDate()
    transaction.setStopDate(None)
    if transaction.getStopDate() != None :
      transaction.setStartDate(None)
      transaction.setStopDate(None)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    transaction.setStartDate(old_start_date)
    transaction.setStopDate(old_stop_date)
    self.getWorkflowTool().doActionFor(transaction, 'stop_action')
    self.assertEquals(transaction.getSimulationState(), 'stopped')
  
  def stepValidateNoSection(self, sequence, sequence_list=None, **kw) :
    """Check validation behaviour related to section & mirror_section.
    When no source section is defined, we are in one of the following
    cases : 
      o if we use payable or receivable account, the validation should
        be refused.
      o if we do not use any payable or receivable accounts and we have
      a destination section, validation should be ok.
    """
    transaction = sequence.get('transaction')
    old_source_section = transaction.getSourceSection()
    old_destination_section = transaction.getDestinationSection()
    # default transaction uses payable accounts, so validating without
    # source section is refused.
    transaction.setSourceSection(None)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    # ... as well as validation without destination section
    transaction.setSourceSection(old_source_section)
    transaction.setDestinationSection(None)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    # mirror section can be set only on the line
    for line in transaction.getMovementList() :
      if line.getSourceValue().isMemberOf(
              'account_type/asset/receivable') or \
         line.getSourceValue().isMemberOf(
              'account_type/liability/payable') :
        line.setDestinationSection(old_destination_section)
    try:
      self.getWorkflowTool().doActionFor(transaction, 'stop_action')
      self.assertEquals(transaction.getSimulationState(), 'stopped')
    except ValidationFailed, err :
      self.assert_(0, "Validation failed : %s" % err.msg)
    
    # if we do not use any payable / receivable account, then we can
    # validate the transaction without setting the mirror section.
    for side in (SOURCE, ): # DESTINATION) :
      # TODO: for now, we only test for source, as it makes no sense to use for
      # destination section only. We could theoritically support it.

      # get a new valid transaction
      self.stepCreateValidAccountingTransaction(sequence)
      transaction = sequence.get('transaction')
      expense_account = sequence.get('expense_account')
      for line in transaction.getMovementList() :
        line.edit( source_value = expense_account,
                   destination_value = expense_account )
      if side == SOURCE :
        transaction.setDestinationSection(None)
      else :
        transaction.setSourceSection(None)
      try:
        self.getWorkflowTool().doActionFor(transaction, 'stop_action')
        self.assertEquals(transaction.getSimulationState(), 'stopped')
      except ValidationFailed, err :
        self.assert_(0, "Validation failed : %s" % err.msg)
        
  def stepValidateNoCurrency(self, sequence, sequence_list=None, **kw) :
    """Check validation behaviour related to currency.
    """
    transaction = sequence.get('transaction')
    old_resource = transaction.getResource()
    transaction.setResource(None)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    # setting a dummy relationship is not enough, resource must be a
    # currency
    transaction.setResource(transaction.getDestinationSection())
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    
  def stepValidateClosedAccount(self, sequence, sequence_list=None, **kw) :
    """Check validation behaviour related to closed accounts.
    If an account is blocked, then it's impossible to validate a
    transaction related to this account.
    """
    transaction = sequence.get('transaction')
    account = transaction.getMovementList()[0].getSourceValue()
    self.getWorkflowTool().doActionFor(account, 'invalidate_action')
    self.assertEquals(account.getValidationState(), 'invalidated')
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    # reopen the account for other tests
    account.validate()
    self.assertEquals(account.getValidationState(), 'validated')
    
  def stepValidateNoAccounts(self, sequence, sequence_list=None, **kw) :
    """Simple check that the validation is refused when we do not have
    accounts correctly defined on lines.
    """
    transaction = sequence.get('transaction')
    # no account at all is refused
    for line in transaction.getMovementList():
      line.setSource(None)
      line.setDestination(None)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    
    # only one line without account and with a quantity is also refused
    self.stepCreateValidAccountingTransaction(sequence)
    transaction = sequence.get('transaction')
    transaction.getMovementList()[0].setSource(None)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    
    # but if we have a line with 0 quantity on both sides, we can
    # validate the transaction and delete this line.
    self.stepCreateValidAccountingTransaction(sequence)
    transaction = sequence.get('transaction')
    line_count = len(transaction.getMovementList())
    transaction.newContent(
        portal_type = self.accounting_transaction_line_portal_type)
    self.getWorkflowTool().doActionFor(transaction, 'stop_action')
    self.assertEquals(transaction.getSimulationState(), 'stopped')
    self.assertEquals(line_count, len(transaction.getMovementList()))
    
    # 0 quantity, but a destination asset price => do not delete the
    # line
    self.stepCreateValidAccountingTransaction(sequence)
    transaction = sequence.get('transaction')
    new_line = transaction.newContent(
        portal_type = self.accounting_transaction_line_portal_type)
    self.assertEquals(len(transaction.getMovementList()), 3)
    line_list = transaction.getMovementList()
    line_list[0].setDestinationTotalAssetPrice(100)
    line_list[0]._setCategoryMembership(
          'destination', sequence.get('expense_account').getRelativeUrl())
    line_list[1].setDestinationTotalAssetPrice(- 50)
    line_list[1]._setCategoryMembership(
          'destination', sequence.get('expense_account').getRelativeUrl())
    line_list[2].setDestinationTotalAssetPrice(- 50)
    line_list[2]._setCategoryMembership(
          'destination', sequence.get('expense_account').getRelativeUrl())
    try:
      self.getWorkflowTool().doActionFor(transaction, 'stop_action')
      self.assertEquals(transaction.getSimulationState(), 'stopped')
    except ValidationFailed, err :
      self.assert_(0, "Validation failed : %s" % err.msg)
  
  def stepValidateNotBalanced(self, sequence, sequence_list=None, **kw) :
    """Check validation behaviour when transaction is not balanced.
    """
    transaction = sequence.get('transaction')
    transaction.getMovementList()[0].setQuantity(4325)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    
    # asset price have priority (ie. if asset price is not balanced,
    # refuses validation even if quantity is balanced)
    self.stepCreateValidAccountingTransaction(sequence)
    transaction = sequence.get('transaction')
    line_list = transaction.getMovementList()
    line_list[0].setDestinationTotalAssetPrice(10)
    line_list[1].setDestinationTotalAssetPrice(100)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    
    self.stepCreateValidAccountingTransaction(sequence)
    transaction = sequence.get('transaction')
    line_list = transaction.getMovementList()
    line_list[0].setSourceTotalAssetPrice(10)
    line_list[1].setSourceTotalAssetPrice(100)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    
    # only asset price needs to be balanced
    self.stepCreateValidAccountingTransaction(sequence)
    transaction = sequence.get('transaction')
    line_list = transaction.getMovementList()
    line_list[0].setSourceTotalAssetPrice(100)
    line_list[0].setDestinationTotalAssetPrice(100)
    line_list[0].setQuantity(432432)
    line_list[1].setSourceTotalAssetPrice(-100)
    line_list[1].setDestinationTotalAssetPrice(-100)
    line_list[1].setQuantity(32546787)
    try:
      self.getWorkflowTool().doActionFor(transaction, 'stop_action')
      self.assertEquals(transaction.getSimulationState(), 'stopped')
    except ValidationFailed, err :
      self.assert_(0, "Validation failed : %s" % err.msg)
  
  def stepValidateNoPayment(self, sequence, sequence_list=None, **kw) :
    """Check validation behaviour related to payment & mirror_payment.
    If we use an account of type asset/cash/bank, we must use set a Bank
    Account as source_payment or destination_payment.
    This this source/destination payment must be a portal type from the
    `payment node` portal type group. It can be defined on transaction
    or line.
    """
    def useBankAccount(transaction):
      """Modify the transaction, so that a line will use an account member of
      account_type/cash/bank , which requires to use a payment category.
      """
      # get the default and replace income account by bank
      income_account_found = 0
      for line in transaction.getMovementList() :
        source_account = line.getSourceValue()
        if source_account.isMemberOf('account_type/income') :
          income_account_found = 1
          line.edit( source_value = sequence.get('bank_account'),
                     destination_value = sequence.get('bank_account') )
      self.failUnless(income_account_found)
    
    transaction = sequence.get('transaction')
    useBankAccount(transaction)
    self.assertRaises(ValidationFailed,
        self.getWorkflowTool().doActionFor,
        transaction,
        'stop_action')
    
    source_section_value = transaction.getSourceSectionValue()
    destination_section_value = transaction.getDestinationSectionValue()
    for ptype in self.getPortal().getPortalPaymentNodeTypeList() :
      source_payment_value = source_section_value.newContent(
                                  portal_type = ptype, )
      destination_payment_value = destination_section_value.newContent(
                                  portal_type = ptype, )
      self.stepCreateValidAccountingTransaction(sequence)
      transaction = sequence.get('transaction')
      useBankAccount(transaction)

      # payment node have to be set on both sides
      transaction.setSourcePaymentValue(source_payment_value)
      transaction.setDestinationPaymentValue(None)
      self.assertRaises(ValidationFailed,
          self.getWorkflowTool().doActionFor,
          transaction,
          'stop_action')
      transaction.setSourcePaymentValue(None)
      transaction.setDestinationPaymentValue(destination_payment_value)
      self.assertRaises(ValidationFailed,
          self.getWorkflowTool().doActionFor,
          transaction,
          'stop_action')
      transaction.setSourcePaymentValue(source_payment_value)
      transaction.setDestinationPaymentValue(destination_payment_value)
      try:
        self.getWorkflowTool().doActionFor(transaction, 'stop_action')
        self.assertEquals(transaction.getSimulationState(), 'stopped')
      except ValidationFailed, err :
        self.assert_(0, "Validation failed : %s" % err.msg)
      
  def stepValidateRemoveEmptyLines(self, sequence, sequence_list=None, **kw):
    """Check validating a transaction remove empty lines. """
    transaction = sequence.get('transaction')
    lines_count = len(transaction.getMovementList())
    empty_lines_count = 0
    for line in transaction.getMovementList():
      if line.getSourceTotalAssetPrice() ==  \
         line.getDestinationTotalAssetPrice() == 0:
        empty_lines_count += 1
    if empty_lines_count == 0:
      transaction.newContent(
            portal_type=self.accounting_transaction_line_portal_type)
    
    self.getWorkflowTool().doActionFor(transaction, 'stop_action')
    self.assertEquals(len(transaction.getMovementList()),
                      lines_count - empty_lines_count)
    
    # we don't remove empty lines if there is only empty lines
    transaction = self.getAccountingModule().newContent(
                      portal_type=self.accounting_transaction_portal_type,
                      created_by_builder=1)
    for i in range(3):
      transaction.newContent(
            portal_type=self.accounting_transaction_line_portal_type)
    lines_count = len(transaction.getMovementList())
    transaction.AccountingTransaction_deleteEmptyLines(redirect=0)
    self.assertEquals(len(transaction.getMovementList()), lines_count)
    
  ############################################################################
  ## Test Methods ############################################################
  ############################################################################
  
  def test_MultiCurrencyInvoice(self, quiet=QUIET, run=RUN_ALL_TESTS):
    """Basic test for multi currency accounting"""
    if not run : return
    self.playSequence("""
      stepCreateCurrencies
      stepCreateEntities
      stepCreateAccounts
      stepCreateInvoices
      stepTic
      stepCheckAccountBalanceLocalCurrency
      stepCheckAccountBalanceExternalCurrency
      stepCheckAccountBalanceConvertedCurrency
    """, quiet=quiet)

  def test_AccountingPeriod(self, quiet=QUIET, run=RUN_ALL_TESTS):
    """Basic test for Accounting Periods"""
    if not run : return
    self.playSequence("""
      stepCreateCurrencies
      stepCreateEntities
      stepCreateAccounts
      stepCreateAccountingPeriod
      stepOpenAccountingPeriod
      stepTic
      stepUseValidDates
      stepCreateInvoices
      stepStopInvoices
      stepCheckInvoicesAreStopped
      stepTic
      stepConfirmAccountingPeriod
      stepTic
      stepDeliverAccountingPeriod
      stepTic
      stepCheckAccountingPeriodDelivered
      stepCheckInvoicesAreDelivered
      stepTic
      stepCheckAccountingTransactionDelivered
    """, quiet=quiet)
  
  def test_AccountingPeriodRefusesWrongDateTransactionValidation(
        self, quiet=QUIET, run=RUN_ALL_TESTS):
    """Accounting Periods prevents transactions to be validated
        when there is no oppened accounting period"""
    if not run : return
    self.playSequence("""
      stepCreateCurrencies
      stepCreateEntities
      stepCreateAccounts
      stepCreateAccountingPeriod
      stepOpenAccountingPeriod
      stepTic
      stepUseInvalidDates
      stepCreateInvoices
      stepCheckStopInvoicesRefused
      stepTic
      stepCheckInvoicesAreDraft
    """, quiet=quiet)

  def test_AccountingPeriodNotStoppedTransactions(self, quiet=QUIET,
                                                  run=RUN_ALL_TESTS):
    """Accounting Periods refuse to close when some transactions are
      not stopped"""
    if not run : return
    self.playSequence("""
      stepCreateCurrencies
      stepCreateEntities
      stepCreateAccounts
      stepCreateAccountingPeriod
      stepOpenAccountingPeriod
      stepTic
      stepCreateInvoices
      stepTic
      stepCheckAccountingPeriodRefusesClosing
      stepTic
      stepCheckInvoicesAreDraft
    """, quiet=quiet)

  def test_AccountingPeriodOtherSections(self, quiet=QUIET,
                                                  run=RUN_ALL_TESTS):
    """Accounting Periods does not change other section transactions."""
    if not run : return
    self.playSequence("""
      stepCreateCurrencies
      stepCreateEntities
      stepCreateAccounts
      stepCreateAccountingPeriod
      stepOpenAccountingPeriod
      stepTic
      stepCreateOtherSectionInvoices
      stepTic
      stepConfirmAccountingPeriod
      stepTic
      stepDeliverAccountingPeriod
      stepTic
      stepCheckAccountingPeriodDelivered
      stepCheckInvoicesAreDraft
    """, quiet=quiet)

  def test_MirrorAccounts(self, quiet=QUIET, run=RUN_ALL_TESTS):
    """Tests using an account on one sides uses the mirror account
    on the other size. """
    if not run : return
    self.playSequence("""
      stepCreateEntities
      stepCreateAccounts
      stepCreateAccountingTransactionAndCheckMirrorAccount
    """, quiet=quiet)

  def test_Acquisition(self, quiet=QUIET, run=RUN_ALL_TESTS):
    """Tests acquisition, categories and portal types are well
    configured. """
    if not run : return
    self.playSequence("""
      stepCreateCurrencies
      stepCheckAcquisition
      """, quiet=quiet)

  def test_AccountingTransactionValidationDate(self, quiet=QUIET,
                                            run=RUN_ALL_TESTS):
    """Transaction validation and dates"""
    if not run : return
    self.playSequence("""
      stepCreateEntities
      stepCreateCurrencies
      stepCreateAccounts
      stepCreateValidAccountingTransaction
      stepValidateNoDate""", quiet=quiet)

  def test_AccountingTransactionValidationSection(self, quiet=QUIET,
                                             run=RUN_ALL_TESTS):
    """Transaction validation and section"""
    if not run : return
    self.playSequence("""
      stepCreateEntities
      stepCreateCurrencies
      stepCreateAccounts
      stepCreateValidAccountingTransaction
      stepValidateNoSection""", quiet=quiet)

  def test_AccountingTransactionValidationCurrency(self, quiet=QUIET,
                                           run=RUN_ALL_TESTS):
    """Transaction validation and currency"""
    if not run : return
    self.playSequence("""
      stepCreateEntities
      stepCreateCurrencies
      stepCreateAccounts
      stepCreateValidAccountingTransaction
      stepValidateNoCurrency""", quiet=quiet)

  def test_AccountingTransactionValidationAccounts(self, quiet=QUIET,
                                           run=RUN_ALL_TESTS):
    """Transaction validation and accounts"""
    if not run : return
    self.playSequence("""
      stepCreateEntities
      stepCreateCurrencies
      stepCreateAccounts
      stepCreateValidAccountingTransaction
      stepValidateClosedAccount
      stepCreateValidAccountingTransaction
      stepValidateNoAccounts""", quiet=quiet)

  def test_AccountingTransactionValidationBalanced(self, quiet=QUIET,
                                              run=RUN_ALL_TESTS):
    """Transaction validation and balance"""
    if not run : return
    self.playSequence("""
      stepCreateEntities
      stepCreateCurrencies
      stepCreateAccounts
      stepCreateValidAccountingTransaction
      stepValidateNotBalanced""", quiet=quiet)

  def test_AccountingTransactionValidationPayment(self, quiet=QUIET,
                                             run=RUN_ALL_TESTS):
    """Transaction validation and payment"""
    if not run : return
    self.playSequence("""
      stepCreateEntities
      stepCreateCurrencies
      stepCreateAccounts
      stepCreateValidAccountingTransaction
      stepValidateNoPayment
    """, quiet=quiet)

  def test_AccountingTransactionValidationRemoveEmptyLines(self, quiet=QUIET,
                                             run=RUN_ALL_TESTS):
    """Transaction validation removes empty lines"""
    if not run : return
    self.playSequence("""
      stepCreateEntities
      stepCreateCurrencies
      stepCreateAccounts
      stepCreateValidAccountingTransaction
      stepValidateRemoveEmptyLines
    """, quiet=quiet)


if __name__ == '__main__':
  framework()
else:
  import unittest
  def test_suite():
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(TestAccounting))
    return suite

