##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
#                    Kevin Deldycke <kevin@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.
#
##############################################################################

from Products.ERP5SyncML.Conduit.ERP5Conduit import ERP5Conduit
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions
from Products.ERP5Type.Utils import convertToUpperCase
from Products.CMFCore.utils import getToolByName
from Acquisition import aq_base, aq_inner, aq_chain, aq_acquire
from ZODB.POSException import ConflictError

import datetime

from zLOG import LOG



class BaobabConduit(ERP5Conduit):

  global property_map

  # Declarative security
  security = ClassSecurityInfo()


  ### This data structure associate a xml property to an ERP5 object property in certain conditions
  property_map = \
    [ { 'xml_property' : 'nom'
      , 'erp5_property': 'first_name'
      , 'conditions'   : {'erp5_portal_type':'Person'}
      }
    , { 'xml_property' : 'nom'
      , 'erp5_property': 'title'
      , 'conditions'   : {'erp5_portal_type':'Organisation'}
      }
    , { 'xml_property' : 'adresse'
      , 'erp5_property': 'default_address_street_address'
      , 'conditions'   : [{'erp5_portal_type':'Organisation'}
                         ,{'erp5_portal_type':'Person'}]
      }
    , { 'xml_property' : 'zone_residence'
      , 'erp5_property': 'default_address_region'
      , 'conditions'   : [{'erp5_portal_type':'Organisation'}
                         ,{'erp5_portal_type':'Person'}]
      }
    , { 'xml_property' : 'titre'
      , 'erp5_property': 'prefix'
      , 'conditions'   : {'erp5_portal_type':'Person'}
      }
    , { 'xml_property' : 'telephone'
      , 'erp5_property': 'default_telephone_number'
      , 'conditions'   : [{'erp5_portal_type':'Organisation'}
                         ,{'erp5_portal_type':'Person'}]
      }
    , { 'xml_property' : 'telex'
      , 'erp5_property': 'default_fax_number'
      , 'conditions'   : [{'erp5_portal_type':'Organisation'}
                         ,{'erp5_portal_type':'Person'}]
      }
    , { 'xml_property' : 'prenom'
      , 'erp5_property': 'last_name'
      , 'conditions'   : {'erp5_portal_type':'Person'}
      }
    , { 'xml_property' : 'date_naissance'
      , 'erp5_property': 'birthday'
      , 'conditions'   : {'erp5_portal_type':'Person'}
      }
    , { 'xml_property' : 'code_bic'
      , 'erp5_property': 'bic_code'
      , 'conditions'   : {'erp5_portal_type':'Organisation'}
      }

    , { 'xml_property' : 'intitule'
      , 'erp5_property': 'title'
      , 'conditions'   : {'erp5_portal_type':'Bank Account'}
      }

    , { 'xml_property' : 'montant_maxi'
      , 'erp5_property': 'operation_upper_limit'
      , 'conditions'   : {'erp5_portal_type':'Agent Privilege'}
      }
    , { 'xml_property' : 'description'
      , 'erp5_property': 'description'
      , 'conditions'   : {'erp5_portal_type':'Agent Privilege'}
      }

    , { 'xml_property' : 'inventory_title'
      , 'erp5_property': 'title'
      , 'conditions'   : {'erp5_portal_type':'Cash Inventory Group'}
      }

    , { 'xml_property' : 'title'
      , 'erp5_property': 'title'
      , 'conditions'   : {'erp5_portal_type':'Bank Account Inventory'}
      }

    , { 'xml_property' : 'amount'
      , 'erp5_property': 'inventory'
      , 'conditions'   : {'erp5_portal_type':'Bank Account Inventory Line'}
      }
    ]



  """
    Methods below are tools to use the property_map.
  """

  security.declarePrivate('buildConditions')
  def buildConditions(self, object):
    """
      Build a condition dictionnary
    """
    dict = {}
    dict['erp5_portal_type'] = object.getPortalType()
    return dict

  security.declarePrivate('findPropertyMapItem')
  def findPropertyMapItem(self, xml_property_name, conditions):
    """
      Find the property_map item that match conditions
    """
    for item in property_map:
      if item['xml_property'] == xml_property_name:
        c = item['conditions']
        if type(c) == type([]):
          if conditions in c:
            return item
        else:
          if conditions == c:
            return item
    return None



  security.declareProtected(Permissions.ModifyPortalContent, 'constructContent')
  def constructContent(self, object, object_id, docid, portal_type):
    """
      This is a redefinition of the original ERP5Conduit.constructContent function to
      create Baobab objects.
    """
    erp5_site_path             = object.absolute_url(relative=1)
    person_module_object       = object.person_module
    organisation_module_object = object.organisation_module

    # Modules below are not always required
    #   (it depends of the nature of objects you want to synchronize)
    try:    cash_inventory_module = object.cash_inventory_module
    except AttributeError: cash_inventory_module = None
    try:    bank_account_inventory_module = object.bank_account_inventory_module
    except AttributeError: bank_account_inventory_module = None
    try:    currency_cash_module = object.currency_cash_module
    except AttributeError: currency_cash_module  = None

    subobject = None

    # Function to search the parent object where the new content must be construct.
    # Given parameter is the special encoded portal type that represent the path to
    #   the wanted destination.
    def findObjectFromSpecialPortalType(special_portal_type):
      source_portal_type = special_portal_type.split('_')[0]
      construction_location = '/'.join(special_portal_type.split('_')[1:][::-1])
      parent_object = None
      for search_folder in ('person_module', 'organisation_module'):
        path = '/' + search_folder + '/' + construction_location
        parent_object_path = erp5_site_path + path
        try:
          parent_object = object.restrictedTraverse(parent_object_path)
        except ConflictError:
          raise
        except:
          LOG( 'BaobabConduit:'
             , 0
             , "expected %s parent object (%s) not found in %s" % ( source_portal_type
                                                                  , construction_location
                                                                  , search_folder
                                                                  )
             )
      if parent_object == None:
        LOG( 'BaobabConduit:'
           , 100
           , "expected %s parent object (%s) not found !" % (source_portal_type, construction_location)
           )
      else:
        LOG( 'BaobabConduit:'
           , 0
           , "%s parent object found at %s" % (source_portal_type, parent_object_path)
           )
      return parent_object

    ### handle client objects
    if portal_type.startswith('Client'):
      if portal_type[-3:] == 'PER':
        subobject = person_module_object.newContent( portal_type = 'Person'
                                                   , id          = object_id
                                                   )
        subobject.setCareerRole('client')
      else:
        subobject = organisation_module_object.newContent( portal_type = 'Organisation'
                                                         , id          = object_id
                                                         )
        subobject.setRole('client')

    ### handle bank account objects
    elif portal_type.startswith('Compte'):
      owner = findObjectFromSpecialPortalType(portal_type)
      if owner == None: return None
      subobject = owner.newContent( portal_type = 'Bank Account'
                                  , id          = object_id
                                  )
      # set the bank account owner as agent with no-limit privileges (only for persons)
      if owner.getPortalType() == 'Person':
        new_agent = subobject.newContent( portal_type = 'Agent'
                                        , id          = 'owner'
                                        )
        new_agent.setAgent(owner.getRelativeUrl())
        privileges = ( 'circularization'
                     , 'cash_out'
                     , 'withdrawal_and_payment'
                     , 'account_document_view'
                     , 'signature'
                     , 'treasury'
                     )
        for privilege in privileges:
          new_priv = new_agent.newContent(portal_type = 'Agent Privilege')
          new_priv.setAgentPrivilege(privilege)

    ### handle agent objects
    elif portal_type.startswith('Mandataire'):
      dest = findObjectFromSpecialPortalType(portal_type)
      if dest == None: return None
      subobject = dest.newContent( portal_type = 'Agent'
                                 , id          = object_id
                                 )
      # try to get the agent in the person module
      person = findObjectFromSpecialPortalType('Person_' + object_id)
      if person == None:
        person = person_module_object.newContent( portal_type = 'Person'
                                                , id          = object_id + 'a'
                                                )
      subobject.setAgent(person.getRelativeUrl())

    ### handle privilege objects
    elif portal_type.startswith('Pouvoir'):
      dest = findObjectFromSpecialPortalType(portal_type)
      if dest == None: return None
      subobject = dest.newContent( portal_type = 'Agent Privilege'
                                 , id          = object_id
                                 )

    ### handle inventory objects
    elif portal_type == 'Cash Inventory':
      if cash_inventory_module == None: return None
      subobject = cash_inventory_module.newContent( portal_type = 'Cash Inventory Group'
                                                  , id          = object_id
                                                  )

    ### handle inventory details objects
    elif portal_type == 'Cash Inventory Detail':
      if currency_cash_module == None: return None
      # get currency and vault informations by analizing the id
      id_items = object_id.split('_')
      if len(id_items) != 5:
        LOG( 'BaobabConduit:'
           , 100
           , "Cash Inventory Detail object has a wrong id (%s) !" % (object_id)
           )
        return None
      cell_id        = id_items[0]
      agency_code    = id_items[1]
      inventory_code = id_items[2]
      vault_code     = id_items[3]
      currency_id    = id_items[4]
      # get the path to the vault_code
      vault_path = self.getVaultPathFromCodification( object         = object
                                                    , agency_code    = agency_code
                                                    , inventory_code = inventory_code
                                                    , vault_code     = vault_code
                                                    , currency_id    = currency_id
                                                    )
      if vault_path in (None, ''):
        LOG( 'BaobabConduit:'
           , 100
           , "can't find a path to the vault '%s/%s/%s' !" % (agency_code, inventory_code, vault_code)
           )
        return None
      # try to find an existing inventory with the same price currency and vault
      inventory_list = object.contentValues(filter={'portal_type': 'Cash Inventory'})
      new_inventory = None
      for inventory in inventory_list:
        inventory_currency = inventory.getPriceCurrencyId()
        inventory_vault    = inventory.getDestination()
        if inventory_currency not in (None, '') and \
           inventory_vault    not in (None, '') and \
           inventory_currency == currency_id    and \
           inventory_vault    == vault_path     :
          new_inventory = inventory
          LOG( 'BaobabConduit:'
             , 0
             , "previous Cash Inventory found (%s) !" % (repr(new_inventory))
             )
          break
      # no previous inventory found, create one
      if new_inventory == None:
        new_inventory = object.newContent(portal_type = 'Cash Inventory')
        new_inventory.setPriceCurrency('currency/' + currency_id)
        new_inventory.setDestination(vault_path)
      subobject = new_inventory

    ### handle bank account inventory objects
    elif portal_type == 'Bank Account Inventory':
      if bank_account_inventory_module == None: return None
      subobject = bank_account_inventory_module.newContent( portal_type = 'Bank Account Inventory'
                                                          , id          = object_id
                                                          )

    ### handle bank account inventory line objects
    elif portal_type == 'Bank Account Inventory Line':
      subobject = object.newContent( portal_type = 'Bank Account Inventory Line'
                                   , id          = object_id
                                   )

    return subobject



  security.declareProtected(Permissions.ModifyPortalContent, 'editDocument')
  def editDocument(self, object=None, **kw):
    """
      This function transfer datas from the dictionary to the baobab document
      object given in parameters.
    """

    if object == None: return

    """
      Write here the code that require to combine more than one property from
      the **kw dictionnary in order to put the right value in object attributes.
    """

    ### Cash Inventory objects needs two properties to generate the vault path
    if object.getPortalType() == 'Cash Inventory Group':
      vault_path = self.getVaultPathFromCodification( object         = object
                                                    , agency_code    = kw['agency_code']
                                                    , inventory_code = kw['inventory_code']
                                                    )
      object.setDestination(vault_path)

    ### Cash Inventory Detail objects needs all properties to create and update the cell matrix
    if object.getPortalType() == 'Cash Inventory':
      quantity      = None
      cell_id       = None
      resource_type = None
      base_price    = None
      currency_name = None
      for k,v in kw.items():
        if k == 'quantity'     : quantity      = float(v)
        if k == 'cell_id'      : cell_id       = v
        if k == 'currency_type': resource_type = v
        if k == 'price'        : base_price    = float(v)
        if k == 'currency'     : currency_name = v
      # try to find an existing line with the same resource as the current cell
      if resource_type in ['BIL']:
        currency_portal_type = 'Banknote'
      elif resource_type in ['MON']:
        currency_portal_type = 'Coin'
      else:
        LOG( 'BaobabConduit:'
           , 100
           , "Cash Inventory Detail resource type can't be guess (%s) !" % (resource_type)
           )
        return None
      # get the list of existing currency to find the currency of the line
      line_currency_cash = None
      currency_cash_list = object.currency_cash_module.contentValues(filter={'portal_type': currency_portal_type})
      for currency_cash in currency_cash_list:
        if base_price    not in (None, '')                    and \
           currency_name not in (None, '')                    and \
           currency_cash.getBasePrice()       == base_price   and \
           currency_cash.getPriceCurrencyId() == currency_name:
          line_currency_cash = currency_cash
          break
      # no currency found
      if line_currency_cash == None:
        LOG( 'BaobabConduit:'
           , 100
           , "Currency '%s %s' not found for the Cash Inventory Detail !" % (base_price, currency_name)
           )
        return None
      # search for lines
      inventory_lines = object.contentValues(filter={'portal_type': 'Cash Inventory Line'})
      new_line = None
      for line in inventory_lines:
        if line.getResourceValue() == line_currency_cash:
          new_line = line
          break
      # no previous line found, create one
      if new_line == None:
        new_line = object.newContent(portal_type = 'Cash Inventory Line')
        new_line.setResourceValue(line_currency_cash)
        new_line.setPrice(line_currency_cash.getBasePrice())
      # get matrix variation values
      category_list = []
      base_cat_map = { 'variation'  : 'variation'
                     , 'letter_code': 'emission_letter'
                     , 'status_code': 'cash_status'
                     }
      for base_key in base_cat_map.keys():
        if base_key in kw.keys() and kw[base_key] not in ('', None):
          if base_key == 'status_code':
            status_table = { 'TVA' : 'valid'
                           , 'NEE' : 'new_emitted'
                           , 'NEU' : 'new_not_emitted'
                           , 'RTC' : 'retired'
                           , 'ATR' : 'to_sort'
                           , 'MUT' : 'mutilated'
                           , 'EAV' : 'to_ventilate'
                           }
            category = status_table[kw[base_key]]
          else:
            category = kw[base_key]
        else:
          category = 'not_defined'
        category_list.append(base_cat_map[base_key] + '/' + category)
      # update the matrix with this cell
      self.updateCashInventoryMatrix( line               = new_line
                                    , cell_category_list = category_list
                                    , quantity           = quantity
                                    , cell_uid           = cell_id
                                    )

    ### Bank Account Inventory Line objects needs two properties to get the right bank account object
    if object.getPortalType() == 'Bank Account Inventory Line':
      currency_id         = None
      bank_account_number = None
      for k,v in kw.items():
        if k == 'currency'      : currency_id         = v
        if k == 'account_number': bank_account_number = v
      # try to find the bank account
      if bank_account_number != None:
        customer_list = object.person_module.contentValues(filter={'portal_type': 'Person'}) + \
                        object.organisation_module.contentValues(filter={'portal_type': 'Organisation'})
        bank_account_object = None
        for customer in customer_list:
          for bank_account in customer.contentValues(filter={'portal_type': 'Bank Account'}):
            if bank_account.getBankAccountNumber() == bank_account_number:
              # found !
              bank_account_object = bank_account
              break
          if bank_account_object != None:
            break
        if bank_account_object != None:
          object.setDestinationValue(bank_account_object)
          if currency_id != None:
            # verify or add the currency
            current_currency_id = bank_account_object.getPriceCurrencyId()
            if current_currency_id in (None, ''):
              bank_account_object.setPriceCurrency('currency/' + currency_id)
            elif current_currency_id != currency_id:
              LOG( 'BaobabConduit inconsistency:'
                 , 200
                 , 'found bank account has not the same currency as expected'
                 )


    """
      Here we use 2 generic way to update object properties :
        1. We try to use the property_map mapping to migrate a value from a property
             to another;
        2. If the latter fail, we try to find a method with a pre-defined name in
             this script to handle the value.
    """

    # Set properties of the destination baobab object
    for k,v in kw.items():
      # Try to find a translation rule in the property_map
      cond = self.buildConditions(object)
      map_item = self.findPropertyMapItem(k, cond)

      ### There is a translation rule, so call the right setProperty() method
      if map_item != None:
        method_id = "set" + convertToUpperCase(map_item['erp5_property'])
        LOG( 'BaobabConduit:'
           , 0
           , "try to call object method %s on %s" % (repr(method_id), repr(object))
           )
        if v not in ('', None):
          if hasattr(object, method_id):
            method = getattr(object, method_id)
            method(v)
          else:
            LOG( 'BaobabConduit:'
               , 100
               , 'property map item don\'t match object properties'
               )

      ### No translation rule found, try to find a hard-coded translation method in the conduit
      else:
        method_id = "edit%s%s" % (kw['type'].replace(' ', ''), convertToUpperCase(k))
        LOG( 'BaobabConduit:'
           , 0
           , "try to call conduit method %s on %s" % (repr(method_id), repr(object))
           )
        if v not in ('', None):
          if hasattr(self, method_id):
            method = getattr(self, method_id)
            method(object, v)
          else:
            LOG( 'BaobabConduit:'
               , 100
               , "there is no method to handle <%s>%s</%s> data" % (k,repr(v),k)
               )




  """
    All functions below are defined to set a document's property to a value
    given in parameters.
    The name of those functions are chosen to help the transfert of datas
    from a given XML format to standard Baobab objects.
  """

  ### Client-related-properties functions

  def editClientCategorie(self, document, value):
    if document.getPortalType() == 'Organisation':
      id_table = { 'BIF': 'institution/world/bank'
                 , 'PFR': 'institution/world/institution'
                 , 'ICU': 'institution/local/common'
                 , 'BET': 'institution/local/institution'
                 , 'ETF': 'institution/local/bank'
                 , 'BTR': 'treasury/national'
                 , 'ORP': 'treasury/other'
                 , 'ORI': 'organism/international'
                 , 'ORR': 'organism/local'
                 , 'COR': 'intermediaries'
                 , 'DIV': 'depositories/various'
                 , 'DER': 'depositories/savings'
                 , 'DAU': 'depositories/other'
                 }
      document.setActivity('banking_finance/' + id_table[value])
    else:
      LOG('BaobabConduit:', 0, 'Person\'s category ignored')

  def editClientNatureEconomique(self, document, value):
    if document.getPortalType() == 'Organisation':
      # build the economical class category path
      c = ''
      path = ''
      for i in value[1:]:
        c += i
        if c == '13':
          path += '/S13'
          if value != 'S13':
            path += '/' + value
          break
        path += '/S' + c
      document.setEconomicalClass(path)
    else:
      LOG( 'BaobabConduit inconsistency:'
         , 200
         , 'a non-Organisation client can\'t have an economical class'
         )

  def editClientSituationMatrimoniale(self, document, value):
    if document.getPortalType() == 'Person':
      id_table = { 'VEU' : 'widowed'
                 , 'DIV' : 'divorced'
                 , 'MAR' : 'married'
                 , 'CEL' : 'never_married'
                 }
      document.setMaritalStatus(id_table[value])
    else:
      LOG( 'BaobabConduit inconsistency:'
         , 200
         , 'a non-Person client can\'t have a marital status'
         )



  ### BankAccount-related-properties functions

  def editCompteDevise(self, document, value):
    document.setPriceCurrency('currency/' + value)

  def editCompteDateOuverture(self, document, value):
    if document.getStopDate() in ('', None):
      document.setStopDate(str(datetime.datetime.max))
    document.setStartDate(value)

  def editCompteDateFermeture(self, document, value):
    if document.getStartDate() in ('', None):
      document.setStartDate(str(datetime.datetime.min))
    document.setStopDate(value)

  def editCompteNumero(self, document, value):
    document.setBankCode(value[0])
    document.setBranch(value[1:3])
    document.setBankAccountNumber(value)



  ### Agent-related-properties functions

  def editMandataireNom(self, document, value):
    old_value = document.getAgentValue().getFirstName()
    new_value = value
    if old_value != new_value:
      LOG( 'BaobabConduit:'
         , 200
         , 'old value of agent first name (%s) was replaced by a new one (%s)' % (old_value, new_value)
         )
      document.getAgentValue().setFirstName(new_value)

  def editMandatairePrenom(self, document, value):
    old_value = document.getAgentValue().getLastName()
    new_value = value
    if old_value != new_value:
      LOG( 'BaobabConduit:'
         , 200
         , 'old value of agent last name (%s) was replaced by a new one (%s)' % (old_value, new_value)
         )
      document.getAgentValue().setLastName(new_value)

  def editMandataireService(self, document, value):
    assignment = document.getAgentValue().newContent( portal_type = 'Assignment'
                                                    , id          = 'service'
                                                    )
    assignment.setGroup(value)
    return

  def editMandataireFonction(self, document, value):
    document.getAgentValue().setCareerGrade(value)
    return

  def editMandataireTelephone(self, document, value):
    old_value = document.getAgentValue().getDefaultTelephoneNumber()
    new_value = value
    if old_value != new_value:
      LOG( 'BaobabConduit:'
         , 200
         , "old value of agent's telephone (%s) was replaced by a new one (%s)" % (old_value, new_value)
         )
      document.getAgentValue().setDefaultTelephoneNumber(new_value)

  def editMandataireDateCreation(self, document, value):
    if document.getStopDate() in ('', None):
      document.setStopDate(str(datetime.datetime.max))
    document.setStartDate(value)



  ### AgentPrivilege-related-properties functions

  def editPouvoirCategorie(self, document, value):
    id_table = { 'COM' : 'clearing'
               , 'CIR' : 'circularization'
               , 'REM' : 'cash_out'
               , 'RET' : 'withdrawal_and_payment'
               , 'RTE' : 'account_document_view'
               , 'SIG' : 'signature'
               , 'TRE' : 'treasury'
               }
    document.setAgentPrivilege(id_table[value])

  def editPouvoirDateDebut(self, document, value):
    if document.getStopDate() in ('', None):
      document.setStopDate(str(datetime.datetime.max))
    document.setStartDate(value)

  def editPouvoirDateFin(self, document, value):
    if document.getStartDate() in ('', None):
      document.setStartDate(str(datetime.datetime.min))
    document.setStopDate(value)



  ### CashInventory-related-properties functions

  def editCashInventoryInventoryDate(self, document, value):
    if value in ('', None):
      date = str(datetime.datetime.max)
    else:
      # Convert french date to strandard date
      date_items = value.split('/')
      day   = date_items[0]
      month = date_items[1]
      year  = date_items[2]
      date  = '/'.join([year, month, day])
    document.setStopDate(date)

  def getVaultPathFromCodification( self, object, agency_code=None, inventory_code=None, vault_code=None, currency_id=None):
    if agency_code in (None, ''):
      return None
    category_tool = object.portal_categories
    # Get the site path to agency
    agency_path = None
    site_base_object = category_tool.resolveCategory('site')
    for site_item in site_base_object.getCategoryChildLogicalPathItemList(base=1)[1:]:
      site_path = site_item[1]
      site_object = category_tool.resolveCategory(site_path)
      if site_object.getPortalType() == 'Category':
        site_code = site_object.getCodification()
        if site_code not in (None, '') and site_code.upper() == agency_code.upper():
          agency_path = site_path
          break
    if inventory_code in (None, ''):
      return agency_path
    # Get the site path corresponding to the inventory type
    inventory_path = None
    agency_site_object = site_object
    for agency_sub_item in agency_site_object.getCategoryChildLogicalPathItemList(base=1)[1:]:
      agency_sub_item_path   = agency_sub_item[1]
      agency_sub_item_object = category_tool.resolveCategory(agency_sub_item_path)
      agency_sub_item_vault  = agency_sub_item_object.getVaultType()
      if agency_sub_item_vault not in (None, ''):
        vault_type_path        = 'vault_type/' + agency_sub_item_vault
        vault_type_object      = category_tool.resolveCategory(vault_type_path)
        vault_type_code        = vault_type_object.getCodification()
        if vault_type_code not in (None, '') and vault_type_code.upper() == inventory_code.upper():
          inventory_path = agency_sub_item_path
          break
    if vault_code in (None, ''):
      return inventory_path
    # Get the site path corresponding to the vault code
    vault_path = None
    vault_site_object = agency_sub_item_object
    for vault_sub_item in vault_site_object.getCategoryChildLogicalPathItemList(base=1)[1:]:
      vault_sub_item_path   = vault_sub_item[1]
      vault_sub_item_object = category_tool.resolveCategory(vault_sub_item_path)
      vault_sub_item_code   = vault_sub_item_object.getCodification()
      if vault_sub_item_code not in (None, '') and vault_sub_item_code.upper() == vault_code.upper():
        vault_path = vault_sub_item_path
        break
    if currency_id in (None, ''):
      return vault_path
    # Get the site path corresponding to the currency-related-subvault
    currency_object = category_tool.currency[currency_id]
    currency_title  = currency_object.getTitle()
    currency_vault_path = None
    vault_object = vault_sub_item_object
    for currency_vault_item in vault_object.getCategoryChildLogicalPathItemList(base=1)[1:]:
      currency_vault_item_path   = currency_vault_item[1]
      currency_vault_item_object = category_tool.resolveCategory(currency_vault_item_path)
      currency_vault_item_title  = currency_vault_item_object.getTitle()
      if currency_vault_item_title not in (None, '') and currency_vault_item_title.upper() == currency_title.upper():
        currency_vault_path = currency_vault_item_path
        break
    if currency_vault_path == None:
      return vault_path
    return currency_vault_path



  ### CashInventoryDetail-related-properties functions

  def updateCashInventoryMatrix(self, line, cell_category_list, quantity, cell_uid):
    base_id = 'movement'
    base_category_list = [ 'emission_letter'
                         , 'variation'
                         , 'cash_status'
                         ]

    old_line_category_list   = line.getVariationCategoryList()
    messy_line_category_list = cell_category_list + old_line_category_list

    sorted_line_base_category_list = []
    sorted_line_category_list = []
    sorted_cell_category_list = []
    sorted_cell_range = []

    # cell_category_list must have the same base category order of cell_range base category
    for base_category in base_category_list:

      # generate the sorted line categories
      for category in messy_line_category_list:
        if category.startswith(base_category + '/') and category not in sorted_line_category_list:
          sorted_line_category_list.append(category)

      # generate the sorted cell range
      base_group = []
      for category in messy_line_category_list:
        if category.startswith(base_category + '/') and category not in base_group:
          base_group.append(category)
      sorted_cell_range.append(base_group)
      # generate the sorted base category
      if len(base_group) > 0:
        sorted_line_base_category_list.append(base_category)

      # generate the sorted cell variation categories
      for category in cell_category_list:
        if category.startswith(base_category + '/') and category not in sorted_cell_category_list:
          sorted_cell_category_list.append(category)

    # update line variation categories
    line.setVariationBaseCategoryList(sorted_line_base_category_list)
    line.setVariationCategoryList(sorted_line_category_list)
    line.setCellRange(base_id = base_id, *sorted_cell_range)
    # create the cell
    kwd = { 'base_id'    : base_id
          , 'portal_type': 'Cash Inventory Cell'
          }
    new_cell = line.newCell(*sorted_cell_category_list, **kwd)
    new_cell.edit( mapped_value_property_list         = ('price', 'inventory')
                 , force_update                       = 1
                 , inventory                          = quantity
                 , membership_criterion_category_list = sorted_cell_category_list
                 , category_list                      = sorted_cell_category_list
                 , title                              = cell_uid
                 )



  ### BankAccountInventory-related-properties functions

  def editBankAccountInventoryAgencyCode(self, document, value):
    agency_path = self.getVaultPathFromCodification( object      = document
                                                   , agency_code = value
                                                   )
    document.setDestination(agency_path)

  def editBankAccountInventoryDate(self, document, value):
    if value in ('', None):
      date = str(datetime.datetime.max)
    else:
      # Convert french date to strandard date
      date_items = value.split('/')
      day   = date_items[0]
      month = date_items[1]
      year  = date_items[2]
      date  = '/'.join([year, month, day])
    document.setStopDate(date)