From 6f3f1d4217abc1f14adb1e4dffd00da04516e5ab Mon Sep 17 00:00:00 2001 From: "dle@openerp.com" <> Date: Fri, 30 Nov 2012 18:20:33 +0100 Subject: [PATCH] [MERGE](Sort of :D)From Luc De Meyer Zip from apps.openerp.com bzr revid: dle@openerp.com-20121130172033-dinqyw8arg79eh31 --- addons/l10n_be_coda/l10n_be_coda.py | 269 +-- addons/l10n_be_coda/l10n_be_coda_view.xml | 169 +- addons/l10n_be_coda/res_bank.py | 33 + addons/l10n_be_coda/res_bank_view.xml | 31 + .../wizard/account_coda_import.py | 1545 +++++++++++++---- 5 files changed, 1577 insertions(+), 470 deletions(-) create mode 100755 addons/l10n_be_coda/res_bank.py create mode 100755 addons/l10n_be_coda/res_bank_view.xml diff --git a/addons/l10n_be_coda/l10n_be_coda.py b/addons/l10n_be_coda/l10n_be_coda.py index 289e757a80d..f9dfa3a686a 100644 --- a/addons/l10n_be_coda/l10n_be_coda.py +++ b/addons/l10n_be_coda/l10n_be_coda.py @@ -2,9 +2,9 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# -# Copyright (c) 2011 Noviat nv/sa (www.noviat.be). All rights reserved. -# +# +# Copyright (c) 2012 Noviat nv/sa (www.noviat.be). All rights reserved. +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -23,20 +23,29 @@ from osv import osv, fields import decimal_precision as dp from tools.translate import _ +import logging +_logger = logging.getLogger(__name__) class coda_bank_account(osv.osv): _name= 'coda.bank.account' _description= 'CODA Bank Account Configuration' + def _check_account_pain(self, cr, uid, context=None): + res = self.pool.get('ir.module.module').search(cr, uid, [('name', '=', 'account_pain'), ('state','=','installed')]) + return res and True or False + def _check_currency(self, cr, uid, ids, context=None): obj_cba = self.browse(cr, uid, ids[0], context=context) - if (obj_cba.state == 'normal') and obj_cba.journal and (obj_cba.currency != obj_cba.journal.currency and (not obj_cba.journal.currency and obj_cba.currency != obj_cba.company_id.currency_id)): - return False + if (obj_cba.state == 'normal') and obj_cba.journal: + if obj_cba.journal.currency and (obj_cba.currency != obj_cba.journal.currency): + return False + if not obj_cba.journal.currency and (obj_cba.currency != obj_cba.company_id.currency_id): + return False return True _columns = { 'name': fields.char('Name', size=64, required=True), - 'bank_id': fields.many2one('res.partner.bank', 'Bank Account', required=True, + 'bank_id': fields.many2one('res.partner.bank', 'Bank Account', required=True, help='Bank Account Number.\nThe CODA import function will find its CODA processing parameters on this number.'), 'description1': fields.char('Primary Account Description', size=35, help='The Primary or Secondary Account Description should match the corresponding Account Description in the CODA file.'), @@ -44,34 +53,47 @@ class coda_bank_account(osv.osv): help='The Primary or Secondary Account Description should match the corresponding Account Description in the CODA file.'), 'state': fields.selection([ ('normal', 'Normal'), - ('info', 'Info')], + ('info', 'Info')], 'Type', required=True, select=1, help='No Bank Statements will be generated for CODA Bank Statements from Bank Accounts of type \'Info\'.'), - 'journal': fields.many2one('account.journal', 'Journal', - domain=[('type', '=', 'bank')], + 'journal': fields.many2one('account.journal', 'Journal', + domain=[('type', '=', 'bank')], states={'normal':[('required',True)],'info':[('required',False)]}, help='Bank Journal for the Bank Statement'), 'currency': fields.many2one('res.currency', 'Currency', required=True, - help='The currency of the CODA Bank Statement'), + help='The currency of the CODA Bank Statement'), 'coda_st_naming': fields.char('Bank Statement Naming Policy', size=64, help="Define the rules to create the name of the Bank Statements generated by the CODA processing." \ - "\nE.g. %(code)s%(y)s/%(paper)s" + "\nE.g. %(code)s%(y)s/%(paper)s" \ "\n\nVariables:" \ "\nBank Journal Code: %(code)s" \ - "\nCurrent Year with Century: %(year)s" \ - "\nCurrent Year without Century: %(y)s" \ + "\nYear (of CODA 'New Balance Date') with Century: %(year)s" \ + "\nYear (of CODA 'New Balance Date') without Century: %(y)s" \ "\nCODA sequence number: %(coda)s" \ - "\nPaper Statement sequence number: %(paper)s"), - 'def_payable': fields.many2one('account.account', 'Default Payable Account', domain=[('type', '=', 'payable')], required=True, - help= 'Set here the payable account that will be used, by default, if the partner is not found.'), - 'def_receivable': fields.many2one('account.account', 'Default Receivable Account', domain=[('type', '=', 'receivable')], required=True, - help= 'Set here the receivable account that will be used, by default, if the partner is not found.',), + "\nPaper Statement sequence number (as specified on 'Old Balance' record): %(paper_ob)s" \ + "\nPaper Statement sequence number (as specified on 'New Balance' record): %(paper)s"), 'awaiting_account': fields.many2one('account.account', 'Default Account for Unrecognized Movement', domain=[('type', '!=', 'view')], required=True, help= 'Set here the default account that will be used if the partner cannot be unambiguously identified.'), 'transfer_account': fields.many2one('account.account', 'Default Internal Transfer Account', domain=[('code', 'like', '58%'), ('type', '!=', 'view')], required=True, help= 'Set here the default account that will be used for internal transfer between own bank accounts (e.g. transfer between current and deposit bank accounts).'), - 'find_bbacom': fields.boolean('Lookup Invoice', required=True, help='Partner lookup via the \'BBA\' Structured Communication field of the Invoice.'), - 'find_partner': fields.boolean('Lookup Partner', required=True, help='Partner lookup via Bank Account Number.'), + 'account_mapping_ids': fields.one2many('coda.account.mapping.rule', 'coda_bank_account_id', 'Account Mapping Rules'), + 'find_payment': fields.boolean('Lookup Payment Reference', + help='Invoice lookup and reconciliation via the SEPA EndToEndReference.' \ + '\nInstall the \'account_pain\' module if you want to take advantage of this feature.'), + 'find_bbacom': fields.boolean('Lookup Structured Communication of type \'BBA\'', + help='Partner lookup and reconciliation via the \'BBA\' Structured Communication.' \ + '\nA partial reconciliation will be created when there is no exact match between the Invoice and Bank Transaction amounts.'), + 'find_inv_number': fields.boolean('Lookup Invoice Number', + help='Partner lookup and reconciliation via the Invoice Number when a communication in free format is used.' \ + '\nA reconciliation will only be created in case of exact match between the Invoice and Bank Transaction amounts.'), + 'find_partner': fields.boolean('Lookup Partner', + help='Partner lookup via Bank Account Number in order to facilitate the reconciliation.'), + 'update_partner': fields.boolean('Update Partner Bank Accounts', + help='Update Partner record when the Counterparty\'s Bank Account has not been registered yet.'), + 'balance_start_enforce': fields.boolean('Prevent invalid Opening Balances', + help="Do not process Statements with an Opening Balance that doesn't match the previous Closing Balance."), + 'discard_dup': fields.boolean('Discard Duplicates', + help="Duplicate Bank Statements will be discarded. Select the corresponding 'CODA Bank Statement' in order to view the contents of such duplicates."), 'active': fields.boolean('Active', help='If the active field is set to False, it will allow you to hide the Bank Account without removing it.'), 'company_id': fields.many2one('res.company', 'Company', required=True), } @@ -79,9 +101,13 @@ class coda_bank_account(osv.osv): 'currency': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id, 'state': 'normal', 'coda_st_naming': '%(code)s/%(y)s/%(coda)s', - 'active': True, + 'active': True, + 'find_payment': _check_account_pain, 'find_bbacom': True, - 'find_partner': True, + 'find_inv_number': True, + 'find_partner': True, + 'update_partner': True, + 'balance_start_enforce': True, 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, } _sql_constraints = [ @@ -107,19 +133,20 @@ class coda_bank_account(osv.osv): if not default: default = {} default = default.copy() - default.update( - journal_id=None, - description1=cba['description1'] or '', - description2=cba['description2'] or '', - name=_("%s (copy)") % (cba['name'] or ''), - state=cba['state']) - return super(coda_bank_account, self).copy(cr, uid, id, default, context) + default.update({'journal_id': None}) + default['description1'] = cba['description1'] or '' + default['description2'] = cba['description2'] or '' + default['name'] = (cba['name'] or '') + ' (copy)' + default['state'] = cba['state'] + return super(coda_bank_account, self).copy(cr, uid, id, default, context) def onchange_state(self, cr, uid, ids, state): return state =='info' and {'value': {'journal': None}} or {} coda_bank_account() + + class account_coda(osv.osv): _name = 'account.coda' _description = 'Object to store CODA Data Files' @@ -138,10 +165,10 @@ class account_coda(osv.osv): 'date': fields.date.context_today, 'user_id': lambda self,cr,uid,context: uid, 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'account.coda', context=c), - } + } _sql_constraints = [ ('coda_uniq', 'unique (name, coda_creation_date)', 'This CODA has already been imported !') - ] + ] def unlink(self, cr, uid, ids, context=None): if context is None: @@ -150,79 +177,73 @@ class account_coda(osv.osv): coda_st_obj = self.pool.get('coda.bank.statement') bank_st_obj = self.pool.get('account.bank.statement') for coda in self.browse(cr, uid, ids, context=context): - for coda_statement in coda.statement_ids: + for coda_statement in coda.statement_ids: if not context.get('coda_statement_unlink', False): if coda_st_obj.exists(cr, uid, coda_statement.id, context=context): - coda_st_obj.unlink(cr, uid, [coda_statement.id], context=context) + coda_st_obj.unlink(cr, uid, [coda_statement.id], context=context) if not context.get('bank_statement_unlink', False): if coda_st_obj.exists(cr, uid, coda_statement.id, context=context) and (coda_statement.type == 'normal') and bank_st_obj.exists(cr, uid, coda_statement.statement_id.id, context=context): - bank_st_obj.unlink(cr, uid, [coda_statement.statement_id.id], context=context) + bank_st_obj.unlink(cr, uid, [coda_statement.statement_id.id], context=context) context.update({'coda_unlink': False}) return super(account_coda, self).unlink(cr, uid, ids, context=context) - + account_coda() -class account_coda_trans_type(osv.osv): +class account_coda_trans_type(osv.osv): _name = 'account.coda.trans.type' _description = 'CODA transaction type' - _rec_name = 'type' + _rec_name = 'type' _columns = { 'type': fields.char('Transaction Type', size=1, required=True), 'parent_id': fields.many2one('account.coda.trans.type', 'Parent'), 'description': fields.text('Description', translate=True), } - account_coda_trans_type() -class account_coda_trans_code(osv.osv): +class account_coda_trans_code(osv.osv): _name = 'account.coda.trans.code' _description = 'CODA transaction code' - _rec_name = 'code' + _rec_name = 'code' _columns = { 'code': fields.char('Code', size=2, required=True, select=1), 'type': fields.selection([ ('code', 'Transaction Code'), - ('family', 'Transaction Family')], - 'Type', required=True, select=1), + ('family', 'Transaction Family')], + 'Type', required=True, select=1), 'parent_id': fields.many2one('account.coda.trans.code', 'Family', select=1), 'description': fields.char('Description', size=128, translate=True, select=2), 'comment': fields.text('Comment', translate=True), } - account_coda_trans_code() -class account_coda_trans_category(osv.osv): +class account_coda_trans_category(osv.osv): _name = 'account.coda.trans.category' _description = 'CODA transaction category' - _rec_name = 'category' + _rec_name = 'category' _columns = { 'category': fields.char('Transaction Category', size=3, required=True), 'description': fields.char('Description', size=256, translate=True), } - account_coda_trans_category() -class account_coda_comm_type(osv.osv): +class account_coda_comm_type(osv.osv): _name = 'account.coda.comm.type' _description = 'CODA structured communication type' - _rec_name = 'code' + _rec_name = 'code' _columns = { 'code': fields.char('Structured Communication Type', size=3, required=True, select=1), 'description': fields.char('Description', size=128, translate=True), } _sql_constraints = [ ('code_uniq', 'unique (code)','The Structured Communication Code must be unique !') - ] - + ] account_coda_comm_type() class coda_bank_statement(osv.osv): - _name = 'coda.bank.statement' - _description = 'CODA Bank Statement' - - def _default_journal_id(self, cr, uid, context=None): - if context is None: - context = {} + _name = 'coda.bank.statement' + _description = 'CODA Bank Statement' + + def _default_journal_id(self, cr, uid, context={}): if context.get('journal_id', False): return context['journal_id'] return False @@ -238,7 +259,7 @@ class coda_bank_statement(osv.osv): res[r] = round(res[r], 2) return res - def _get_period(self, cr, uid, context=None): + def _get_period(self, cr, uid, context={}): periods = self.pool.get('account.period').find(cr, uid) if periods: return periods[0] @@ -249,19 +270,22 @@ class coda_bank_statement(osv.osv): _columns = { 'name': fields.char('Name', size=64, required=True, readonly=True), 'date': fields.date('Date', required=True, readonly=True), + 'coda_creation_date': fields.date('CODA Creation Date', readonly=True), + 'old_balance_date': fields.date('Old Balance Date', readonly=True), + 'new_balance_date': fields.date('New Balance Date', readonly=True), 'coda_id': fields.many2one('account.coda', 'CODA Data File', ondelete='cascade'), 'type': fields.selection([ ('normal', 'Normal'), - ('info', 'Info')], + ('info', 'Info')], 'Type', required=True, readonly=True, help='No Bank Statements are associated with CODA Bank Statements of type \'Info\'.'), - 'statement_id': fields.many2one('account.bank.statement', 'Associated Bank Statement'), + 'statement_id': fields.many2one('account.bank.statement', 'Associated Bank Statement'), 'journal_id': fields.many2one('account.journal', 'Journal', readonly=True, domain=[('type', '=', 'bank')]), - 'coda_bank_account_id': fields.many2one('coda.bank.account', 'Bank Account', readonly=True), + 'coda_bank_account_id': fields.many2one('coda.bank.account', 'Bank Account', readonly=True), 'period_id': fields.many2one('account.period', 'Period', required=True, readonly=True), 'balance_start': fields.float('Starting Balance', digits_compute=dp.get_precision('Account'), readonly=True), 'balance_end_real': fields.float('Ending Balance', digits_compute=dp.get_precision('Account'), readonly=True), - 'balance_end': fields.function(_end_balance, method=True, store=True, string='Balance'), + 'balance_end': fields.function(_end_balance, method=True, store=True, string='Balance'), 'line_ids': fields.one2many('coda.bank.statement.line', 'statement_id', 'CODA Bank Statement lines', readonly=True), 'currency': fields.many2one('res.currency', 'Currency', required=True, readonly=True, @@ -269,14 +293,14 @@ class coda_bank_statement(osv.osv): 'company_id': fields.related('journal_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), } _defaults = { - 'type': 'normal', + 'type': 'normal', 'currency': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id, 'journal_id': _default_journal_id, 'period_id': _get_period, } def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): - if context is None: + if context is None: context = {} res = super(coda_bank_statement, self).search(cr, uid, args=args, offset=offset, limit=limit, order=order, context=context, count=count) @@ -290,20 +314,20 @@ class coda_bank_statement(osv.osv): context.update({'coda_statement_unlink': True}) coda_obj = self.pool.get('account.coda') bank_st_obj = self.pool.get('account.bank.statement') - + # find all CODA bank statements that are associated with the selected CODA bank statements via a common CODA file - new_ids = [] + new_ids = [] for coda_statement in self.browse(cr, uid, ids, context=context): if coda_obj.exists(cr, uid, coda_statement.coda_id.id, context=context): new_ids += [x.id for x in coda_obj.browse(cr, uid, coda_statement.coda_id.id, context=context).statement_ids] - # unlink CODA banks statements as well as associated bank statements and CODA files + # unlink CODA banks statements as well as associated bank statements and CODA files for coda_statement in self.browse(cr, uid, new_ids, context=context): - if coda_statement.statement_id.state == 'confirm': - raise osv.except_osv(_('Invalid Action!'), - _("Cannot delete CODA Bank Statement '%s' of journal '%s'." \ - "\nThe associated Bank Statement has already been confirmed." \ - "\nPlease undo this action first.") \ + if coda_statement.statement_id.state == 'confirm': + raise osv.except_osv(_('Invalid action !'), + _("Cannot delete CODA Bank Statement '%s' of Journal '%s'." \ + "\nThe associated Bank Statement has already been confirmed !" \ + "\nPlease undo this action first!") \ % (coda_statement.name, coda_statement.journal_id.name)) else: if not context.get('coda_unlink', False): @@ -311,11 +335,11 @@ class coda_bank_statement(osv.osv): coda_obj.unlink(cr, uid, [coda_statement.coda_id.id], context=context) if not context.get('bank_statement_unlink', False): if coda_statement.statement_id and bank_st_obj.exists(cr, uid, coda_statement.statement_id.id, context=context): - bank_st_obj.unlink(cr, uid, [coda_statement.statement_id.id], context=context) + bank_st_obj.unlink(cr, uid, [coda_statement.statement_id.id], context=context) context.update({'coda_statement_unlink': False}) return super(coda_bank_statement, self).unlink(cr, uid, new_ids, context=context) - + coda_bank_statement() class account_bank_statement(osv.osv): @@ -323,7 +347,10 @@ class account_bank_statement(osv.osv): _columns = { 'coda_statement_id': fields.many2one('coda.bank.statement', 'Associated CODA Bank Statement'), } - + _sql_constraints = [ + ('name_company_uniq', 'unique (name,company_id)', 'The name of the Bank Statement must be unique per company !') + ] + def unlink(self, cr, uid, ids, context=None): if context is None: context = {} @@ -332,15 +359,15 @@ class account_bank_statement(osv.osv): coda_st_obj = self.pool.get('coda.bank.statement') # find all statements that are associated with the selected bank statements via a common CODA file - ids_plus = [] + ids_plus = [] for statement in self.browse(cr, uid, ids, context=context): if statement.coda_statement_id: for x in coda_obj.browse(cr, uid, statement.coda_statement_id.coda_id.id, context=context).statement_ids: if x.type == 'normal': ids_plus += [x.statement_id.id] - + # unlink banks statements as well as associated CODA bank statements and CODA files - for statement in self.browse(cr, uid, ids_plus, context=context): + for statement in self.browse(cr, uid, ids_plus, context=context): if not context.get('coda_statement_unlink', False): if statement.coda_statement_id and coda_st_obj.exists(cr, uid, statement.coda_statement_id.id, context=context): coda_st_obj.unlink(cr, uid, [statement.coda_statement_id.id], context=context) @@ -354,63 +381,109 @@ class account_bank_statement(osv.osv): context.update({'bank_statement_unlink': False}) new_ids = list(set(ids + ids_plus)) return super(account_bank_statement, self).unlink(cr, uid, new_ids, context=context) - + account_bank_statement() class coda_bank_statement_line(osv.osv): - _name = 'coda.bank.statement.line' - _order = 'sequence' + _name = 'coda.bank.statement.line' + _order = 'sequence' _description = 'CODA Bank Statement Line' _columns = { 'name': fields.char('Communication', size=268, required=True), 'sequence': fields.integer('Sequence'), 'date': fields.date('Entry Date', required=True), - 'val_date': fields.date('Valuta Date'), - 'account_id': fields.many2one('account.account','Account'), # remove required=True + 'val_date': fields.date('Valuta Date'), + 'account_id': fields.many2one('account.account','Account'), 'type': fields.selection([ ('supplier','Supplier'), ('customer','Customer'), ('general','General'), - ('globalisation','Globalisation'), - ('information','Information'), - ('communication','Free Communication'), + ('globalisation','Globalisation'), + ('information','Information'), + ('communication','Free Communication'), ], 'Type', required=True), - 'globalisation_level': fields.integer('Globalisation Level', - help="The value which is mentioned (1 to 9), specifies the hierarchy level" - " of the globalisation of which this record is the first." - "\nThe same code will be repeated at the end of the globalisation."), - 'globalisation_amount': fields.float('Globalisation Amount', digits_compute=dp.get_precision('Account')), + 'globalisation_level': fields.integer('Globalisation Level', + help="The value which is mentioned (1 to 9), specifies the hierarchy level" + " of the globalisation of which this record is the first." + "\nThe same code will be repeated at the end of the globalisation."), + 'globalisation_amount': fields.float('Globalisation Amount', digits_compute=dp.get_precision('Account')), 'globalisation_id': fields.many2one('account.bank.statement.line.global', 'Globalisation ID', readonly=True, - help="Code to identify transactions belonging to the same globalisation level within a batch payment"), + help="Code to identify transactions belonging to the same globalisation level within a batch payment"), 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')), 'partner_id': fields.many2one('res.partner', 'Partner'), 'counterparty_name': fields.char('Counterparty Name', size=35), - 'counterparty_bic': fields.char('Counterparty BIC', size=11), - 'counterparty_number': fields.char('Counterparty Number', size=34), - 'counterparty_currency': fields.char('Counterparty Currency', size=3), + 'counterparty_bic': fields.char('Counterparty BIC', size=11), + 'counterparty_number': fields.char('Counterparty Number', size=34), + 'counterparty_currency': fields.char('Counterparty Currency', size=3), + 'payment_reference': fields.char('Payment Reference', size=35, + help="Payment Reference. For SEPA (SCT or SDD) transactions, the PaymentInformationIdentification "\ + "is recorded in this field pertaining to a globalisation, and the EndToEndReference for " \ + "simple transactions or for the details of a globalisation."), 'statement_id': fields.many2one('coda.bank.statement', 'CODA Bank Statement', select=True, required=True, ondelete='cascade'), 'coda_bank_account_id': fields.related('statement_id', 'coda_bank_account_id', type='many2one', relation='coda.bank.account', string='Bank Account', store=True, readonly=True), 'ref': fields.char('Reference', size=32), 'note': fields.text('Notes'), - 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), + 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), } def unlink(self, cr, uid, ids, context=None): if context is None: context = {} if context.get('block_statement_line_delete', False): - raise osv.except_osv('Warning', _('Delete operation not allowed.')) + raise osv.except_osv('Warning', _('Delete operation not allowed !')) return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context) -coda_bank_statement_line() +coda_bank_statement_line() class account_bank_statement_line_global(osv.osv): _inherit = 'account.bank.statement.line.global' _columns = { 'coda_statement_line_ids': fields.one2many('coda.bank.statement.line', 'globalisation_id', 'CODA Bank Statement Lines', readonly=True), } - account_bank_statement_line_global() +class coda_account_mapping_rule(osv.osv): + _name = 'coda.account.mapping.rule' + _description = 'Rules Engine to assign accounts during CODA parsing' + _order = 'sequence' + _columns = { + 'coda_bank_account_id': fields.many2one('coda.bank.account', 'CODA Bank Account', ondelete='cascade'), + 'sequence': fields.integer('Sequence', help='Determines the order of the rules to assign accounts'), + 'name': fields.char('Rule Name', size=128, required=True), + # matching criteria + 'trans_type_id': fields.many2one('account.coda.trans.type', 'Transaction Type'), + 'trans_family_id': fields.many2one('account.coda.trans.code', 'Transaction Family', domain=[('type', '=', 'family')]), + 'trans_code_id': fields.many2one('account.coda.trans.code', 'Transaction Code', domain=[('type', '=', 'code')]), + 'trans_category_id': fields.many2one('account.coda.trans.category', 'Transaction Category'), + 'struct_comm_type_id': fields.many2one('account.coda.comm.type', 'Structured Communication Type'), + 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='cascade'), + # resulting general account + 'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', required=True, domain=[('type', '!=', 'view')]), + 'active': fields.boolean('Active', help='Switch on/off this rule.'), + } + _defaults = { + 'active': True, + } + + def account_id_get(self, cr, uid, coda_bank_account_id, trans_type_id=None, trans_family_id=None, + trans_code_id=None, trans_category_id=None, struct_comm_type_id=None, partner_id=None, context=None): + cr.execute('SELECT account_id, trans_type_id, trans_family_id, trans_code_id, trans_category_id, struct_comm_type_id, partner_id ' \ + 'FROM coda_account_mapping_rule ' \ + 'WHERE active = TRUE AND coda_bank_account_id = %s ' \ + 'ORDER BY sequence' % coda_bank_account_id + ) + rules = cr.fetchall() + condition = '(not rule[1] or (trans_type_id == rule[1])) and (not rule[2] or (trans_family_id == rule[2])) ' \ + 'and (not rule[3] or (trans_code_id == rule[3])) and (not rule[4] or (trans_category_id == rule[4])) ' \ + 'and (not rule[5] or (struct_comm_type_id == rule[5])) and (not rule[6] or (partner_id == rule[6]))' + account_id = None + for rule in rules: + if eval(condition): + account_id = rule[0] + break + return account_id + +coda_account_mapping_rule() + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/l10n_be_coda/l10n_be_coda_view.xml b/addons/l10n_be_coda/l10n_be_coda_view.xml index 1f014400d3c..fb0a12b73fb 100644 --- a/addons/l10n_be_coda/l10n_be_coda_view.xml +++ b/addons/l10n_be_coda/l10n_be_coda_view.xml @@ -11,10 +11,11 @@ coda.bank.account - + + @@ -54,15 +55,52 @@ - - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+
@@ -282,11 +320,12 @@ coda.bank.statement.list coda.bank.statement - + - - + + + @@ -299,61 +338,56 @@ coda.bank.statement.form coda.bank.statement -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -362,10 +396,12 @@ coda.bank.statement - - + + + + @@ -457,6 +493,7 @@ + diff --git a/addons/l10n_be_coda/res_bank.py b/addons/l10n_be_coda/res_bank.py new file mode 100755 index 00000000000..6a3581539e9 --- /dev/null +++ b/addons/l10n_be_coda/res_bank.py @@ -0,0 +1,33 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# +# Copyright (c) 2012 Noviat nv/sa (www.noviat.be). All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from osv import fields, osv + +class res_bank(osv.osv): + _inherit = 'res.bank' + _columns = { + 'code': fields.char('Code', size=3, + help='Country specific Bank Code (used for bban to iban conversion).'), + } +res_bank() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/l10n_be_coda/res_bank_view.xml b/addons/l10n_be_coda/res_bank_view.xml new file mode 100755 index 00000000000..fe4a71d2bdb --- /dev/null +++ b/addons/l10n_be_coda/res_bank_view.xml @@ -0,0 +1,31 @@ + + + + + + res.bank.form.add.code + res.bank + + form + + + + + + + + + res.bank.tree.add.code + res.bank + + form + + + + + + + + + + diff --git a/addons/l10n_be_coda/wizard/account_coda_import.py b/addons/l10n_be_coda/wizard/account_coda_import.py index 46c545f0f48..6f055ca3d92 100644 --- a/addons/l10n_be_coda/wizard/account_coda_import.py +++ b/addons/l10n_be_coda/wizard/account_coda_import.py @@ -2,9 +2,9 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# -# Copyright (c) 2011 Noviat nv/sa (www.noviat.be). All rights reserved. -# +# +# Copyright (c) 2012 Noviat nv/sa (www.noviat.be). All rights reserved. +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -24,11 +24,11 @@ import time import base64 from osv import fields,osv from tools.translate import _ -import logging +import netsvc import re from traceback import format_exception from sys import exc_info -from base_iban import base_iban +import logging _logger = logging.getLogger(__name__) class account_coda_import(osv.osv_memory): @@ -43,6 +43,10 @@ class account_coda_import(osv.osv_memory): 'coda_fname': lambda *a: '', } + def _check_account_payment(self, cr, uid, context=None): + res = self.pool.get('ir.module.module').search(cr, uid, [('name', '=', 'account_payment'), ('state','=','installed')]) + return res and True or False + def coda_parsing(self, cr, uid, ids, context=None, batch=False, codafile=None, codafilename=None): if context is None: context = {} @@ -53,12 +57,12 @@ class account_coda_import(osv.osv_memory): data=self.browse(cr,uid,ids)[0] try: codafile = data.coda_data - codafilename = data.coda_fname + codafilename = data.coda_fname except: - raise osv.except_osv(_('Error!'), _('Wizard in incorrect state. Please hit the Cancel button.')) + raise osv.except_osv(_('Error!'), _('Wizard in incorrect state. Please hit the Cancel button!')) return {} - currency_obj = self.pool.get('res.currency') + currency_obj = self.pool.get('res.currency') coda_bank_account_obj = self.pool.get('coda.bank.account') trans_type_obj = self.pool.get('account.coda.trans.type') trans_code_obj = self.pool.get('account.coda.trans.code') @@ -80,13 +84,23 @@ class account_coda_import(osv.osv_memory): voucher_line_obj = self.pool.get('account.voucher.line') seq_obj = self.pool.get('ir.sequence') mod_obj = self.pool.get('ir.model.data') + partner_obj = self.pool.get('res.partner') + account_mapping_obj = self.pool.get('coda.account.mapping.rule') + + if self._check_account_payment(cr, uid): + payment_line_obj = self.pool.get('payment.line') + else: + payment_line_obj = False + + wf_service = netsvc.LocalService('workflow') + be_country_id = self.pool.get('res.country').search(cr, uid, [('code', '=', 'BE')])[0] coda_bank_table = coda_bank_account_obj.read(cr, uid, coda_bank_account_obj.search(cr, uid, []), context=context) for coda_bank in coda_bank_table: coda_bank.update({'journal_code': coda_bank['journal'] and journal_obj.browse(cr, uid, coda_bank['journal'][0], context=context).code or ''}) coda_bank.update({'iban': partner_bank_obj.browse(cr, uid, coda_bank['bank_id'][0], context=context).iban}) coda_bank.update({'acc_number': partner_bank_obj.browse(cr, uid, coda_bank['bank_id'][0], context=context).acc_number}) - coda_bank.update({'currency_name': currency_obj.browse(cr, uid, coda_bank['currency'][0], context=context).name}) + coda_bank.update({'currency_name': currency_obj.browse(cr, uid, coda_bank['currency'][0], context=context).name}) trans_type_table = trans_type_obj.read(cr, uid, trans_type_obj.search(cr, uid, []), context=context) trans_code_table = trans_code_obj.read(cr, uid, trans_code_obj.search(cr, uid, []), context=context) trans_category_table = trans_category_obj.read(cr, uid, trans_category_obj.search(cr, uid, []), context=context) @@ -97,9 +111,10 @@ class account_coda_import(osv.osv_memory): err_log = '' coda_statements = [] recordlist = unicode(base64.decodestring(codafile), 'windows-1252', 'strict').split('\n') - + digits = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account') + for line in recordlist: - + if not line: pass elif line[0] == '0': @@ -108,42 +123,35 @@ class account_coda_import(osv.osv_memory): coda_parsing_note = '' coda_statement_lines = {} st_line_seq = 0 - glob_lvl_stack = [0] # header data - coda_statement['currency'] = 'EUR' # default currency + coda_statement['currency'] = 'EUR' # default currency coda_statement['version'] = line[127] coda_version = line[127] if coda_version not in ['1','2']: - err_string = _('\nCODA V%s statements are not supported, please contact your bank.') % coda_version + err_string = _('\nCODA V%s statements are not supported, please contact your bank!') % coda_version err_code = 'R0001' if batch: return (err_code, err_string) raise osv.except_osv(_('Data Error!'), err_string) coda_statement['coda_statement_lines'] = {} coda_statement['date'] = str2date(line[5:11]) - period_id = period_obj.search(cr , uid, [('date_start' ,'<=', coda_statement['date']), ('date_stop','>=',coda_statement['date'])]) - if not period_id: - err_string = _("\nThe CODA creation date doesn't fall within a defined Accounting Period." \ - "\nPlease create the Accounting Period for date %s.") % coda_statement['date'] - err_code = 'R0002' - if batch: - return (err_code, err_string) - raise osv.except_osv(_('Data Error!'), err_string) - coda_statement['period_id'] = period_id[0] + coda_statement['coda_creation_date'] = str2date(line[5:11]) + coda_statement['separate_application'] = line[83:88] + coda_statement['first_transaction_date'] = False coda_statement['state'] = 'draft' - + coda_id = coda_obj.search(cr, uid,[ ('name', '=', codafilename), ('coda_creation_date', '=', coda_statement['date']), ]) if coda_id: - err_string = _("\nCODA File with Filename '%s' and Creation Date '%s' has already been imported.") \ + err_string = _("\nCODA File with Filename '%s' and Creation Date '%s' has already been imported !") \ % (codafilename, coda_statement['date']) err_code = 'W0001' if batch: return (err_code, err_string) - raise osv.except_osv(_('Warning!'), err_string) - + raise osv.except_osv(_('Warning !'), err_string) + elif line[0] == '1': if coda_version == '1': coda_statement['acc_number'] = line[5:17] @@ -151,47 +159,56 @@ class account_coda_import(osv.osv_memory): coda_statement['currency'] = line[18:21] elif line[1] == '0': # Belgian bank account BBAN structure coda_statement['acc_number'] = line[5:17] - coda_statement['currency'] = line[18:21] + coda_statement['currency'] = line[18:21] elif line[1] == '1': # foreign bank account BBAN structure - err_string = _('\nForeign bank accounts with BBAN structure are not supported.') + err_string = _('\nForeign bank accounts with BBAN structure are not supported !') err_code = 'R1001' if batch: return (err_code, err_string) raise osv.except_osv(_('Data Error!'), err_string) elif line[1] == '2': # Belgian bank account IBAN structure - coda_statement['acc_number']=line[5:21] + coda_statement['acc_number']=line[5:21] coda_statement['currency'] = line[39:42] elif line[1] == '3': # foreign bank account IBAN structure - err_string = _('\nForeign bank accounts with IBAN structure are not supported.') + err_string = _('\nForeign bank accounts with IBAN structure are not supported !') err_code = 'R1002' if batch: return (err_code, err_string) raise osv.except_osv(_('Data Error!'), err_string) else: - err_string = _('\nUnsupported bank account structure.') + err_string = _('\nUnsupported bank account structure !') err_code = 'R1003' if batch: return (err_code, err_string) raise osv.except_osv(_('Data Error!'), err_string) coda_statement['description'] = line[90:125].strip() - cba_filter = lambda x: ((coda_statement['acc_number'] in (x['iban'] or '')) or (coda_statement['acc_number'] == x['acc_number'])) \ - and (coda_statement['currency'] == x['currency_name']) and (coda_statement['description'] == (x['description1'] or x['description2'] or '')) + cba_filter = lambda x: (coda_statement['acc_number'] in _get_acc_numbers(x['acc_number'])) \ + and (coda_statement['currency'] == x['currency_name']) and (coda_statement['description'] in [x['description1'] or '', x['description2'] or '']) coda_bank = filter(cba_filter, coda_bank_table) if coda_bank: - coda_bank = coda_bank[0] + coda_bank = coda_bank[0] coda_statement['type'] = coda_bank['state'] coda_statement['journal_id'] = coda_bank['journal'] and coda_bank['journal'][0] coda_statement['currency_id'] = coda_bank['currency'][0] - coda_statement['coda_bank_account_id'] = coda_bank['id'] - def_pay_acc = coda_bank['def_payable'][0] - def_rec_acc = coda_bank['def_receivable'][0] + coda_statement['coda_bank_account_id'] = coda_bank['id'] + coda_statement['account_mapping_ids'] = coda_bank['account_mapping_ids'] + coda_statement['coda_bank_params'] = coda_bank awaiting_acc = coda_bank['awaiting_account'][0] transfer_acc = coda_bank['transfer_account'][0] + find_payment = coda_bank['find_payment'] find_bbacom = coda_bank['find_bbacom'] + find_inv_number = coda_bank['find_inv_number'] find_partner = coda_bank['find_partner'] + update_partner = coda_bank['update_partner'] + coda_statement['balance_start_enforce'] = coda_bank['balance_start_enforce'] + coda_statement['discard_dup'] = coda_bank['discard_dup'] + company_id = coda_bank['company_id'][0] + company_bank_ids = partner_bank_obj.search(cr, uid, [('company_id', '=', company_id)]) + company_bank_accounts = partner_bank_obj.read(cr, uid, company_bank_ids, ['acc_number']) + company_bank_accounts = [x['acc_number'].replace(' ', '') for x in company_bank_accounts] else: - err_string = _("\nNo matching CODA Bank Account Configuration record found.") + \ - _("\nPlease check if the 'Bank Account Number', 'Currency' and 'Account Description' fields of your configuration record match with '%s', '%s' and '%s'.") \ + err_string = _("\nNo matching CODA Bank Account Configuration record found !") + \ + _("\nPlease check if the 'Bank Account Number', 'Currency' and 'Account Description' fields of your configuration record match with '%s', '%s' and '%s' !") \ % (coda_statement['acc_number'], coda_statement['currency'], coda_statement['description']) err_code = 'R1004' if batch: @@ -200,56 +217,72 @@ class account_coda_import(osv.osv_memory): bal_start = list2float(line[43:58]) # old balance data if line[42] == '1': # 1= Debit bal_start = - bal_start - coda_statement['balance_start'] = bal_start + coda_statement['balance_start'] = bal_start + coda_statement['old_balance_date'] = str2date(line[58:64]) coda_statement['acc_holder'] = line[64:90] - coda_statement['paper_seq_number'] = line[2:5] + coda_statement['paper_ob_seq_number'] = line[2:5] coda_statement['coda_seq_number'] = line[125:128] - if coda_bank['coda_st_naming']: - coda_statement['name'] = coda_bank['coda_st_naming'] % { - 'code': coda_bank['journal_code'] or '', - 'year': time.strftime('%Y'), - 'y': time.strftime('%y'), - 'coda': line[125:128], - 'paper': line[2:5], - } - else: - coda_statement['name'] = '/' elif line[0] == '2': # movement data record 2 + if line[1] == '1': # movement data record 2.1 st_line = {} st_line_seq = st_line_seq + 1 st_line['sequence'] = st_line_seq st_line['type'] = 'general' - st_line['reconcile'] = False + st_line['reconcile'] = False + st_line['trans_family'] = False st_line['struct_comm_type'] = '' + st_line['struct_comm_type_id'] = 0 st_line['struct_comm_type_desc'] = '' - st_line['struct_comm_101'] = '' + st_line['struct_comm_bba'] = '' st_line['communication'] = '' + st_line['payment_reference'] = '' st_line['partner_id'] = 0 st_line['account_id'] = 0 st_line['counterparty_name'] = '' - st_line['counterparty_bic'] = '' + st_line['counterparty_bic'] = '' st_line['counterparty_number'] = '' - st_line['counterparty_currency'] = '' + st_line['counterparty_currency'] = '' st_line['glob_lvl_flag'] = False st_line['globalisation_id'] = 0 st_line['globalisation_code'] = '' st_line['globalisation_amount'] = False st_line['amount'] = False - + st_line['ref'] = line[2:10] + st_line['ref_move'] = line[2:6] + st_line['ref_move_detail'] = line[6:10] + + if st_line_seq == 1: + main_move_stack = [st_line] # initialise main_move_stack (used to link 2.1 detail records to 2.1 main record + glob_lvl_stack = [0] # initialise globalisation stack + elif st_line['ref_move_detail'] == '0000': + glob_lvl_stack = [0] # re-initialise globalisation stack + st_line['trans_ref'] = line[10:31] st_line_amt = list2float(line[32:47]) if line[31] == '1': # 1=debit st_line_amt = - st_line_amt - # processing of amount depending on globalisation code + + st_line['trans_type'] = line[53] + trans_type = filter(lambda x: st_line['trans_type'] == x['type'], trans_type_table) + if not trans_type: + err_string = _('\nThe File contains an invalid CODA Transaction Type : %s!') % st_line['trans_type'] + err_code = 'R2001' + if batch: + return (err_code, err_string) + raise osv.except_osv(_('Data Error!'), err_string) + st_line['trans_type_id'] = trans_type[0]['id'] + st_line['trans_type_desc'] = trans_type[0]['description'] + + # processing of amount depending on globalisation code glob_lvl_flag = int(line[124]) - if glob_lvl_flag > 0: - if glob_lvl_stack[-1] == glob_lvl_flag: - st_line['glob_lvl_flag'] = glob_lvl_flag + if glob_lvl_flag > 0: + if glob_lvl_stack[-1] == glob_lvl_flag: + st_line['glob_lvl_flag'] = glob_lvl_flag st_line['amount'] = st_line_amt glob_lvl_stack.pop() else: @@ -257,199 +290,388 @@ class account_coda_import(osv.osv_memory): st_line['type'] = 'globalisation' st_line['glob_lvl_flag'] = glob_lvl_flag st_line['globalisation_amount'] = st_line_amt - st_line['account_id'] = None else: st_line['amount'] = st_line_amt - # positions 48-53 : Value date or 000000 if not known (DDMMYY) + + # The 'globalisation' concept can also be implemented without the globalisation level flag. + # This is e.g. used by Europabank to give the details of Card Payments. + if st_line['ref_move'] == main_move_stack[-1]['ref_move']: + if st_line['ref_move_detail'] == '9999': + # Current CODA parsing logic doesn't support > 9999 detail lines + err_string = _('\nTransaction Detail Limit reached!') + err_code = 'R2010' + if batch: + return (err_code, err_string) + raise osv.except_osv(_('Data Error!'), err_string) + elif st_line['ref_move_detail'] != '0000': + if glob_lvl_stack[-1] == 0: + # promote associated move record into a globalisation + glob_lvl_flag = 1 + glob_lvl_stack.append(glob_lvl_flag) + main_st_line_seq = main_move_stack[-1]['sequence'] + to_promote = coda_statement['coda_statement_lines'][main_st_line_seq] + if not main_move_stack[-1].get('detail_cnt'): + to_promote.update({ + 'type': 'globalisation', + 'glob_lvl_flag': glob_lvl_flag, + 'globalisation_amount': main_move_stack[-1]['amount'], + 'amount': False, + 'account_id': 0, + }) + main_move_stack[-1]['promoted'] = True + if not main_move_stack[-1].get('detail_cnt'): + main_move_stack[-1]['detail_cnt'] = 1 + else: + main_move_stack[-1]['detail_cnt'] += 1 + + # positions 48-53 : Valuta date or 000000 if not known (DDMMYY) st_line['val_date'] = str2date(line[47:53]) # positions 54-61 : transaction code - st_line['trans_type'] = line[53] - trans_type = filter(lambda x: st_line['trans_type'] == x['type'], trans_type_table) - if not trans_type: - err_string = _('\nThe File contains an invalid CODA Transaction Type : %s.') % st_line['trans_type'] - err_code = 'R2001' - if batch: - return (err_code, err_string) - raise osv.except_osv(_('Data Error!'), err_string) - st_line['trans_type_desc'] = trans_type[0]['description'] st_line['trans_family'] = line[54:56] trans_family = filter(lambda x: (x['type'] == 'family') and (st_line['trans_family'] == x['code']), trans_code_table) if not trans_family: - err_string = _('\nThe File contains an invalid CODA Transaction Family : %s.') % st_line['trans_family'] + err_string = _('\nThe File contains an invalid CODA Transaction Family : %s!') % st_line['trans_family'] err_code = 'R2002' if batch: return (err_code, err_string) - raise osv.except_osv(_('Data Error!'), err_string) + raise osv.except_osv(_('Data Error!'), err_string) + st_line['trans_family_id'] = trans_family[0]['id'] st_line['trans_family_desc'] = trans_family[0]['description'] st_line['trans_code'] = line[56:58] - trans_code = filter(lambda x: (x['type'] == 'code') and (st_line['trans_code'] == x['code']) and (trans_family[0]['id'] == x['parent_id'][0]), + trans_code = filter(lambda x: (x['type'] == 'code') and (st_line['trans_code'] == x['code']) and (trans_family[0]['id'] == x['parent_id'][0]), trans_code_table) if trans_code: + st_line['trans_code_id'] = trans_code[0]['id'] st_line['trans_code_desc'] = trans_code[0]['description'] else: + st_line['trans_code_id'] = None st_line['trans_code_desc'] = _('Transaction Code unknown, please consult your bank.') st_line['trans_category'] = line[58:61] trans_category = filter(lambda x: st_line['trans_category'] == x['category'], trans_category_table) if trans_category: + st_line['trans_category_id'] = trans_category[0]['id'] st_line['trans_category_desc'] = trans_category[0]['description'] else: - st_line['trans_category_desc'] = _('Transaction Category unknown, please consult your bank.') - # positions 61-115 : communication + st_line['trans_category_id'] = None + st_line['trans_category_desc'] = _('Transaction Category unknown, please consult your bank.') + # positions 61-115 : communication if line[61] == '1': st_line['struct_comm_type'] = line[62:65] comm_type = filter(lambda x: st_line['struct_comm_type'] == x['code'], comm_type_table) if not comm_type: - err_string = _('\nThe File contains an invalid Structured Communication Type : %s.') % st_line['struct_comm_type'] + err_string = _('\nThe File contains an invalid Structured Communication Type : %s!') % st_line['struct_comm_type'] err_code = 'R2003' if batch: return (err_code, err_string) raise osv.except_osv(_('Data Error!'), err_string) + st_line['struct_comm_type_id'] = comm_type[0]['id'] st_line['struct_comm_type_desc'] = comm_type[0]['description'] st_line['communication'] = st_line['name'] = line[65:115] - if st_line['struct_comm_type'] == '101': - bbacomm = line[65:77] - st_line['struct_comm_101'] = st_line['name'] = '+++' + bbacomm[0:3] + '/' + bbacomm[3:7] + '/' + bbacomm[7:] + '+++' + if st_line['struct_comm_type'] in ['101', '102']: + bbacomm = line[65:77] + st_line['struct_comm_bba'] = st_line['name'] = '+++' + bbacomm[0:3] + '/' + bbacomm[3:7] + '/' + bbacomm[7:] + '+++' else: st_line['communication'] = st_line['name'] = line[62:115] st_line['entry_date'] = str2date(line[115:121]) - # positions 122-124 not processed - coda_statement_lines[st_line_seq] = st_line + if st_line['sequence'] == 1: + coda_statement['first_transaction_date'] = st_line['entry_date'] + # positions 122-124 not processed + coda_statement_lines[st_line_seq] = st_line # store transaction + + if st_line['ref_move'] != main_move_stack[-1]['ref_move']: + if main_move_stack[-1].get('detail_cnt') and main_move_stack[-1].get('promoted'): + # add closing globalisation level on previous detail record in order to correctly close + # moves that have been 'promoted' to globalisation + closeglobalise = coda_statement['coda_statement_lines'][st_line_seq-1] + closeglobalise.update({ + 'glob_lvl_flag': main_move_stack[-1]['glob_lvl_flag'], + }) + else: + # demote record with globalisation code from 'globalisation' to 'general' when no detail records + # the same logic is repeated on the New Balance Record ('8 Record') in order to cope with CODA files + # containing a single 2.1 record that needs to be 'demoted'. + if main_move_stack[-1]['type'] == 'globalisation' and not main_move_stack[-1].get('detail_cnt'): + # demote record with globalisation code from 'globalisation' to 'general' when no detail records + main_st_line_seq = main_move_stack[-1]['sequence'] + to_demote = coda_statement['coda_statement_lines'][main_st_line_seq] + to_demote.update({ + 'type': 'general', + 'glob_lvl_flag': 0, + 'globalisation_amount': False, + 'amount': main_move_stack[-1]['globalisation_amount'], + }) + main_move_stack.pop() + main_move_stack.append(st_line) coda_statement['coda_statement_lines'] = coda_statement_lines + elif line[1] == '2': # movement data record 2.2 - if coda_statement['coda_statement_lines'][st_line_seq]['ref'] != line[2:10]: - err_string = _('\nCODA parsing error on movement data record 2.2, seq nr %s.' \ + if coda_statement['coda_statement_lines'][st_line_seq]['ref'][0:4] != line[2:6]: + err_string = _('\nCODA parsing error on movement data record 2.2, seq nr %s!' \ '\nPlease report this issue via your OpenERP support channel.') % line[2:10] err_code = 'R2004' if batch: return (err_code, err_string) - raise osv.except_osv(_('Error!'), err_string) + raise osv.except_osv(_('Error!'), err_string) coda_statement['coda_statement_lines'][st_line_seq]['name'] += line[10:63] coda_statement['coda_statement_lines'][st_line_seq]['communication'] += line[10:63] + coda_statement['coda_statement_lines'][st_line_seq]['payment_reference'] = line[63:98].strip() coda_statement['coda_statement_lines'][st_line_seq]['counterparty_bic'] = line[98:109].strip() + elif line[1] == '3': # movement data record 2.3 - if coda_statement['coda_statement_lines'][st_line_seq]['ref'] != line[2:10]: - err_string = _('\nCODA parsing error on movement data record 2.3, seq nr %s.' \ + if coda_statement['coda_statement_lines'][st_line_seq]['ref'][0:4] != line[2:6]: + err_string = _('\nCODA parsing error on movement data record 2.3, seq nr %s!' \ '\nPlease report this issue via your OpenERP support channel.') % line[2:10] err_code = 'R2005' if batch: return (err_code, err_string) - raise osv.except_osv(_('Error!'), err_string) + raise osv.except_osv(_('Error!'), err_string) st_line = coda_statement_lines[st_line_seq] if coda_version == '1': - counterparty_number = line[10:22] + counterparty_number = line[10:22].strip() counterparty_name = line[47:125].strip() counterparty_currency = '' else: if line[22] == ' ': - counterparty_number = line[10:22] + counterparty_number = line[10:22].strip() counterparty_currency = line[23:26].strip() else: counterparty_number = line[10:44].strip() - counterparty_currency = line[44:47].strip() + counterparty_currency = line[44:47].strip() counterparty_name = line[47:82].strip() st_line['name'] += line[82:125] st_line['communication'] += line[82:125] st_line['counterparty_number'] = counterparty_number st_line['counterparty_currency'] = counterparty_currency st_line['counterparty_name'] = counterparty_name + """ + TO DO: + + replace code infra by check on flag 128 and copy info in Notes Field. + if counterparty_currency not in [coda_bank['currency_name'], '']: - err_string = _('\nCODA parsing error on movement data record 2.3, seq nr %s.' \ - '\nPlease report this issue via your OpenERP support channel.') % line[2:10] + err_string = _('\nCODA parsing error on movement data record 2.3, seq nr %s!' \ + '\nPlease report this issue via your OpenERP support channel.') % line[2:10] err_code = 'R2006' if batch: return (err_code, err_string) - raise osv.except_osv(_('Error!'), err_string) + raise osv.except_osv(_('Error!'), err_string) + """ - # partner matching and reconciliation - if st_line['type'] == 'general': + # invoice matching and reconciliation + if st_line['type'] == 'general': match = False - bank_ids = False - # prepare reconciliation for bba scor - reference = st_line['struct_comm_101'] - if reference and find_bbacom: - inv_ids = inv_obj.search(cr , uid, [('reference' ,'=', reference), ('reference_type' ,'=', 'bba')]) - if inv_ids: - invoice = inv_obj.browse(cr, uid, inv_ids[0]) - partner = invoice.partner_id - st_line['partner_id'] = partner.id - if invoice.type in ['in_invoice', 'in_refund']: - st_line['account_id'] = partner.property_account_payable.id or def_pay_acc - st_line['type'] = 'supplier' + payment_reference_match = False + inv_ids = None + partner_bank_ids = False + + # check payment reference in bank statement line against payment order lines + payment_reference = st_line['payment_reference'] + if payment_reference and find_payment and st_line['amount'] < 0: + payline_ids = payment_line_obj and payment_line_obj.search(cr, uid, [('name', '=', payment_reference)]) + if payline_ids: + if len(payline_ids) == 1: + payline = payment_line_obj.browse(cr, uid, payline_ids[0]) + match = True + payment_reference_match = True + st_line['partner_id'] = payline.partner_id.id + if payline.move_line_id: + st_line['reconcile'] = payline.move_line_id.id + st_line['account_id'] = payline.move_line_id.account_id.id + if payline._get_ml_inv_ref: + inv_type = payline.ml_inv_ref.type + st_line['type'] = inv_type == 'in_invoice' and 'supplier' or 'customer' + else: + st_line['type'] = 'general' else: - st_line['account_id'] = partner.property_account_receivable.id or def_rec_acc - st_line['type'] = 'customer' - if invoice.type in ['in_invoice', 'out_invoice']: - iml_ids = move_line_obj.search(cr, uid, [('move_id', '=', invoice.move_id.id), ('reconcile_id', '=', False), ('account_id.reconcile', '=', True)]) - if iml_ids: - st_line['reconcile'] = iml_ids[0] - match = True + err_string = _('\nThe CODA parsing detected a payment reference ambiguity while processing movement data record 2.3, seq nr %s!' \ + '\nPlease check your Payment Gateway configuration or contact your OpenERP support channel.') % line[2:10] + err_code = 'R2007' + if batch: + return (err_code, err_string) + raise osv.except_osv(_('Error!'), err_string) + + # check bba scor in bank statement line against open invoices + if st_line['struct_comm_bba'] and find_bbacom: + if st_line['amount'] > 0: + domain = [('type', 'in', ['out_invoice', 'in_refund'])] else: - coda_parsing_note += _("\n Bank Statement '%s' line '%s':" \ - "\n There is no invoice matching the Structured Communication '%s'." \ + domain = [('type', 'in', ['in_invoice', 'out_refund'])] + inv_ids = inv_obj.search(cr , uid, + domain + [('state', '=', 'open'), ('reference' ,'=', st_line['struct_comm_bba']), ('reference_type' ,'=', 'bba')]) + if not inv_ids: + coda_parsing_note += _("\n Bank Statement '%%(name)s' line '%s':" \ + "\n There is no invoice matching the Structured Communication '%s'!" \ "\n Please verify and adjust the invoice and perform the import again or otherwise change the corresponding entry manually in the generated Bank Statement.") \ - % (coda_statement['name'], st_line['ref'], reference) - # lookup partner via counterparty_number + % (st_line['ref'], st_line['struct_comm_bba']) + elif len(inv_ids) == 1: + match = True + elif len(inv_ids) > 1: + coda_parsing_note += _("\n Bank Statement '%%(name)s' line '%s':" \ + "\n There are multiple invoices matching the Structured Communication '%s'!" \ + "\n A manual reconciliation is required.") \ + % (st_line['ref'], st_line['struct_comm_bba']) + + # use free comm in bank statement line for lookup against open invoices + if not match and find_bbacom: + # extract possible bba scor from free form communication and try to find matching invoice + free_comm_digits = re.sub('\D', '', st_line['communication'] or '') + select = "SELECT id FROM (SELECT id, type, state, amount_total, number, reference_type, reference, " \ + "'%s'::text AS free_comm_digits FROM account_invoice) sq " \ + "WHERE state = 'open' AND reference_type = 'bba' " \ + "AND free_comm_digits LIKE '%%'||regexp_replace(reference, '\\\D', '', 'g')||'%%'" \ + % (free_comm_digits) + if st_line['amount'] > 0: + select2 = " AND type IN ('out_invoice', 'in_refund')" + else: + select2 = " AND type IN ('in_invoice', 'out_refund')" + cr.execute(select + select2) + res=cr.fetchall() + if res: + inv_ids = [x[0] for x in res] + if len(inv_ids) == 1: + match = True + if not match and st_line['communication'] and find_inv_number: + # check matching invoice number in free form communication combined with matching amount + free_comm = repl_special(st_line['communication'].strip()) + amount_fmt = '%.' + str(digits) + 'f' + if st_line['amount'] > 0: + amount_rounded = amount_fmt % round(st_line['amount'], digits) + else: + amount_rounded = amount_fmt % round(-st_line['amount'], digits) + select = "SELECT id FROM (SELECT id, type, state, amount_total, number, reference_type, reference, " \ + "'%s'::text AS free_comm FROM account_invoice) sq " \ + "WHERE state = 'open' AND amount_total = %s" \ + % (free_comm, amount_rounded) + # 'out_invoice', 'in_refund' + if st_line['amount'] > 0: + select2 = " AND type = 'out_invoice' AND free_comm ilike '%'||number||'%'" + cr.execute(select + select2) + res=cr.fetchall() + if res: + inv_ids = [x[0] for x in res] + else: + select2 = " AND type = 'in_refund' AND free_comm ilike '%'||reference||'%'" + cr.execute(select + select2) + res=cr.fetchall() + if res: + inv_ids = [x[0] for x in res] + # 'in_invoice', 'out_refund' + else: + select2 = " AND type = 'in_invoice' AND free_comm ilike '%'||reference||'%'" + cr.execute(select + select2) + res=cr.fetchall() + if res: + inv_ids = [x[0] for x in res] + else: + select2 = " AND type = 'out_refund' AND free_comm ilike '%'||number||'%'" + cr.execute(select + select2) + res=cr.fetchall() + if res: + inv_ids = [x[0] for x in res] + if inv_ids: + if len(inv_ids) == 1: + match = True + elif len(inv_ids) > 1: + coda_parsing_note += _("\n Bank Statement '%%(name)s' line '%s':" \ + "\n There are multiple invoices matching the Invoice Amount and Reference." \ + "\n A manual reconciliation is required.") \ + % (st_line['ref']) + + if not payment_reference_match and match and inv_ids and len(inv_ids) == 1: + invoice = inv_obj.browse(cr, uid, inv_ids[0]) + partner = invoice.partner_id + st_line['partner_id'] = partner.id + if invoice.type in ['in_invoice', 'in_refund']: + st_line['account_id'] = partner.property_account_payable.id + st_line['type'] = 'supplier' + else: + st_line['account_id'] = partner.property_account_receivable.id + st_line['type'] = 'customer' + iml_ids = move_line_obj.search(cr, uid, [('move_id', '=', invoice.move_id.id), ('reconcile_id', '=', False), ('account_id.reconcile', '=', True)]) + if iml_ids: + st_line['reconcile'] = iml_ids[0] + else: + err_string = _('\nThe CODA parsing detected a database inconsistency while processing movement data record 2.3, seq nr %s!' \ + '\nPlease report this issue via your OpenERP support channel.') % line[2:10] + err_code = 'R2008' + if batch: + return (err_code, err_string) + raise osv.except_osv(_('Error!'), err_string) + + # lookup partner via counterparty_number when invoice lookup failed if not match and counterparty_number: - cba_filter = lambda x: ((counterparty_number in (x['iban'] or '')) or (counterparty_number == x['acc_number'])) \ - and (x['state'] == 'normal') - transfer_account = filter(cba_filter, coda_bank_table) + transfer_account = filter(lambda x: counterparty_number in x, company_bank_accounts) if transfer_account: st_line['account_id'] = transfer_acc match = True elif find_partner: - counterparty_number = base_iban._pretty_iban(base_iban._format_iban(counterparty_number)) - bank_ids = partner_bank_obj.search(cr,uid,[('acc_number','=', counterparty_number)]) - if not match and find_partner and bank_ids: - if len(bank_ids) > 1: - coda_parsing_note += _("\n Bank Statement '%s' line '%s':" \ - "\n No partner record assigned: There are multiple partners with the same Bank Account Number '%s'." \ + partner_bank_ids = partner_bank_obj.search(cr,uid,[('acc_number','=', counterparty_number)]) + if not match and find_partner and partner_bank_ids: + if len(partner_bank_ids) > 1: + coda_parsing_note += _("\n Bank Statement '%%(name)s' line '%s':" \ + "\n No partner record assigned: There are multiple partners with the same Bank Account Number '%s'!" \ "\n Please correct the configuration and perform the import again or otherwise change the corresponding entry manually in the generated Bank Statement.") \ - % (coda_statement['name'], st_line['ref'], counterparty_number) - else: - bank = partner_bank_obj.browse(cr, uid, bank_ids[0], context) - st_line['partner_id'] = bank.partner_id.id + % (st_line['ref'], counterparty_number) + else: + partner_bank = partner_bank_obj.browse(cr, uid, partner_bank_ids[0], context) + st_line['partner_id'] = partner_bank.partner_id.id match = True if st_line['amount'] < 0: - st_line['account_id'] = bank.partner_id.property_account_payable.id or def_pay_acc + st_line['account_id'] = partner_bank.partner_id.property_account_payable.id st_line['type'] = 'supplier' else: - st_line['account_id'] = bank.partner_id.property_account_receivable.id or def_rec_acc + st_line['account_id'] = partner_bank.partner_id.property_account_receivable.id st_line['type'] = 'customer' elif not match and find_partner: if counterparty_number: - coda_parsing_note += _("\n Bank Statement '%s' line '%s':" \ - "\n The bank account '%s' is not defined for the partner '%s'." \ + coda_parsing_note += _("\n Bank Statement '%%(name)s' line '%s':" \ + "\n The bank account '%s' is not defined for the partner '%s'!" \ "\n Please correct the configuration and perform the import again or otherwise change the corresponding entry manually in the generated Bank Statement.") \ - % (coda_statement['name'], st_line['ref'], - counterparty_number, counterparty_name) + % (st_line['ref'], counterparty_number, counterparty_name) else: - coda_parsing_note += _("\n Bank Statement '%s' line '%s':" \ - "\n No matching partner record found." \ + coda_parsing_note += _("\n Bank Statement '%%(name)s' line '%s':" \ + "\n No matching partner record found!" \ "\n Please adjust the corresponding entry manually in the generated Bank Statement.") \ - % (coda_statement['name'], st_line['ref']) + % (st_line['ref']) st_line['account_id'] = awaiting_acc - # end of partner record lookup + + # add bank account to partner record + if match and st_line['account_id'] != transfer_acc and counterparty_number and update_partner: + partner_bank_ids = partner_bank_obj.search(cr,uid,[('acc_number','=', counterparty_number), ('partner_id', '=', st_line['partner_id'])], order='id') + if len(partner_bank_ids) > 1: + # clean up partner bank duplicates, keep most recently created + _logger.warn('Duplicate Bank Accounts for partner_id %s have been removed, ids = %s', + st_line['partner_id'], partner_bank_ids[:-1]) + partner_bank_obj.unlink(cr,uid, partner_bank_ids[:-1]) + if not partner_bank_ids: + feedback = update_partner_bank(self, cr, uid, st_line['counterparty_bic'], counterparty_number, st_line['partner_id'], counterparty_name) + if feedback: + coda_parsing_note += _("\n Bank Statement '%%(name)s' line '%s':") % st_line['ref'] + feedback + coda_statement_lines[st_line_seq] = st_line coda_statement['coda_statement_lines'] = coda_statement_lines + # end of processing movement data record 2.3 + else: # movement data record 2.x (x <> 1,2,3) - err_string = _('\nMovement data records of type 2.%s are not supported.') % line[1] - err_code = 'R2007' + err_string = _('\nMovement data records of type 2.%s are not supported !') % line[1] + err_code = 'R2009' if batch: return (err_code, err_string) - raise osv.except_osv(_('Data Error!'), err_string) + raise osv.except_osv(_('Data Error!'), err_string) elif line[0] == '3': # information data record 3 + if line[1] == '1': # information data record 3.1 info_line = {} info_line['entry_date'] = st_line['entry_date'] info_line['type'] = 'information' st_line_seq = st_line_seq + 1 - info_line['sequence'] = st_line_seq + info_line['sequence'] = st_line_seq info_line['struct_comm_type'] = '' info_line['struct_comm_type_desc'] = '' info_line['communication'] = '' @@ -463,19 +685,19 @@ class account_coda_import(osv.osv_memory): err_code = 'R3001' if batch: return (err_code, err_string) - raise osv.except_osv(_('Data Error!'), err_string) - info_line['trans_type_desc'] = trans_type[0]['description'] + raise osv.except_osv(_('Data Error!'), err_string) + info_line['trans_type_desc'] = trans_type[0]['description'] info_line['trans_family'] = line[32:34] trans_family = filter(lambda x: (x['type'] == 'family') and (info_line['trans_family'] == x['code']), trans_code_table) if not trans_family: - err_string = _('\nThe File contains an invalid CODA Transaction Family : %s.') % st_line['trans_family'] + err_string = _('\nThe File contains an invalid CODA Transaction Family : %s!') % st_line['trans_family'] err_code = 'R3002' if batch: return (err_code, err_string) - raise osv.except_osv(_('Data Error!'), err_string) + raise osv.except_osv(_('Data Error!'), err_string) info_line['trans_family_desc'] = trans_family[0]['description'] info_line['trans_code'] = line[34:36] - trans_code = filter(lambda x: (x['type'] == 'code') and (info_line['trans_code'] == x['code']) and (trans_family[0]['id'] == x['parent_id']), + trans_code = filter(lambda x: (x['type'] == 'code') and (info_line['trans_code'] == x['code']) and (trans_family[0]['id'] == x['parent_id'][0]), trans_code_table) if trans_code: info_line['trans_code_desc'] = trans_code[0]['description'] @@ -486,13 +708,13 @@ class account_coda_import(osv.osv_memory): if trans_category: info_line['trans_category_desc'] = trans_category[0]['description'] else: - info_line['trans_category_desc'] = _('Transaction Category unknown, please consult your bank.') - # positions 40-113 : communication + info_line['trans_category_desc'] = _('Transaction Category unknown, please consult your bank.') + # positions 40-113 : communication if line[39] == '1': info_line['struct_comm_type'] = line[40:43] comm_type = filter(lambda x: info_line['struct_comm_type'] == x['code'], comm_type_table) if not comm_type: - err_string = _('\nThe File contains an invalid Structured Communication Type : %s.') % info_line['struct_comm_type'] + err_string = _('\nThe File contains an invalid Structured Communication Type : %s!') % info_line['struct_comm_type'] err_code = 'R3003' if batch: return (err_code, err_string) @@ -504,21 +726,23 @@ class account_coda_import(osv.osv_memory): # positions 114-128 not processed coda_statement_lines[st_line_seq] = info_line coda_statement['coda_statement_lines'] = coda_statement_lines + elif line[1] == '2': # information data record 3.2 if coda_statement['coda_statement_lines'][st_line_seq]['ref'] != line[2:10]: - err_string = _('\nCODA parsing error on information data record 3.2, seq nr %s.' \ + err_string = _('\nCODA parsing error on information data record 3.2, seq nr %s!' \ '\nPlease report this issue via your OpenERP support channel.') % line[2:10] err_code = 'R3004' if batch: return (err_code, err_string) raise osv.except_osv(_('Error!'), err_string) - coda_statement['coda_statement_lines'][st_line_seq]['name'] += line[10:115] + coda_statement['coda_statement_lines'][st_line_seq]['name'] += line[10:115] coda_statement['coda_statement_lines'][st_line_seq]['communication'] += line[10:115] + elif line[1] == '3': # information data record 3.3 if coda_statement['coda_statement_lines'][st_line_seq]['ref'] != line[2:10]: - err_string = _('\nCODA parsing error on information data record 3.3, seq nr %s.' \ + err_string = _('\nCODA parsing error on information data record 3.3, seq nr %s!' \ '\nPlease report this issue via your OpenERP support channel.') % line[2:10] err_code = 'R3005' if batch: @@ -526,7 +750,7 @@ class account_coda_import(osv.osv_memory): raise osv.except_osv(_('Error!'), err_string) coda_statement['coda_statement_lines'][st_line_seq]['name'] += line[10:100] coda_statement['coda_statement_lines'][st_line_seq]['communication'] += line[10:100] - + elif line[0] == '4': # free communication data record 4 comm_line = {} @@ -537,21 +761,64 @@ class account_coda_import(osv.osv_memory): comm_line['communication'] = comm_line['name'] = line[32:112] coda_statement_lines[st_line_seq] = comm_line coda_statement['coda_statement_lines'] = coda_statement_lines - + elif line[0] == '8': # new balance record + last_transaction = main_move_stack[-1] + if last_transaction['type'] == 'globalisation' and not last_transaction.get('detail_cnt'): + # demote record with globalisation code from 'globalisation' to 'general' when no detail records + main_st_line_seq = main_move_stack[-1]['sequence'] + to_demote = coda_statement['coda_statement_lines'][main_st_line_seq] + to_demote.update({ + 'type': 'general', + 'glob_lvl_flag': 0, + 'globalisation_amount': False, + 'amount': main_move_stack[-1]['globalisation_amount'], + }) + # add closing globalisation level on previous detail record in order to correctly close + # moves that have been 'promoted' to globalisation + if main_move_stack[-1].get('detail_cnt') and main_move_stack[-1].get('promoted'): + closeglobalise = coda_statement['coda_statement_lines'][st_line_seq-1] + closeglobalise.update({ + 'glob_lvl_flag': main_move_stack[-1]['glob_lvl_flag'], + }) + coda_statement['paper_nb_seq_number'] = line[1:4] bal_end = list2float(line[42:57]) + coda_statement['new_balance_date'] = str2date(line[57:63]) if line[41] == '1': # 1=Debit bal_end = - bal_end coda_statement['balance_end_real'] = bal_end - + if coda_statement['new_balance_date']: + period_id = period_obj.search(cr , uid, [('date_start' ,'<=', coda_statement['new_balance_date']), ('date_stop','>=',coda_statement['new_balance_date'])]) + else: + period_id = period_obj.search(cr , uid, [('date_start' ,'<=', coda_statement['date']), ('date_stop','>=',coda_statement['date'])]) + if not period_id: + err_string = _("\nThe CODA Statement New Balance date doesn't fall within a defined Accounting Period!" \ + "\nPlease create the Accounting Period for date %s.") % coda_statement['new_balance_date'] + err_code = 'R0002' + if batch: + return (err_code, err_string) + raise osv.except_osv(_('Data Error!'), err_string) + coda_statement['period_id'] = period_id[0] + if coda_bank['coda_st_naming']: + coda_statement['name'] = coda_bank['coda_st_naming'] % { + 'code': coda_bank['journal_code'] or '', + 'year': coda_statement['new_balance_date'] and coda_statement['new_balance_date'][:4] or coda_statement['date'][:4], + 'y': coda_statement['new_balance_date'] and coda_statement['new_balance_date'][2:4] or coda_statement['date'][2:4], + 'coda': coda_statement['coda_seq_number'], + 'paper_ob': coda_statement['paper_ob_seq_number'], + 'paper': coda_statement['paper_nb_seq_number'], + } + else: + coda_statement['name'] = '/' + elif line[0] == '9': # footer record - coda_statement['balance_min'] = list2float(line[22:37]) + coda_statement['balance_min'] = list2float(line[22:37]) coda_statement['balance_plus'] = list2float(line[37:52]) if not bal_end: coda_statement['balance_end_real'] = coda_statement['balance_start'] + coda_statement['balance_plus'] - coda_statement['balance_min'] - if coda_parsing_note: + if coda_parsing_note: coda_statement['coda_parsing_note'] = '\nStatement Line matching results:' + coda_parsing_note else: coda_statement['coda_parsing_note'] = '' @@ -559,11 +826,11 @@ class account_coda_import(osv.osv_memory): #end for err_string = '' - err_code = '' + err_code = '' coda_id = 0 coda_note = '' line_note = '' - + try: coda_id = coda_obj.create(cr, uid,{ 'name' : codafilename, @@ -573,7 +840,7 @@ class account_coda_import(osv.osv_memory): 'user_id': uid, }) context.update({'coda_id': coda_id}) - + except osv.except_osv, e: cr.rollback() err_string = _('\nApplication Error : ') + str(e) @@ -587,78 +854,127 @@ class account_coda_import(osv.osv_memory): err_code = 'G0001' if batch: return (err_code, err_string) - raise osv.except_osv(_('CODA Import failed.'), err_string) + raise osv.except_osv(_('CODA Import failed !'), err_string) nb_err = 0 err_string = '' coda_st_ids = [] - bk_st_ids = [] - + bk_st_ids = [] + for statement in coda_statements: - + # The CODA Statement info is written to two objects: 'coda.bank.statement' and 'account.bank.statement' try: - + normal2info = False + lines = statement['coda_statement_lines'] + if not statement['first_transaction_date']: + normal2info = True # don't create a bank statement for CODA's without transactions + else: + line_vals = [x for x in lines.itervalues()] + transactions = filter(lambda x: (x['type'] in ['globalisation', 'general', 'supplier', 'customer']) and x['amount'], line_vals) + if not transactions: + normal2info = True # don't create a bank statement for CODA's without transactions + if normal2info: + statement['type'] = 'info' # don't create a bank statement for CODA's without transactions + statement['coda_parsing_note'] += _("\n\nThe CODA Statement %s does not contain transactions, hence no Bank Statement has been created." \ + "\nSelect the 'CODA Bank Statement' to check the contents of %s.") \ + %(statement['name'], statement['name']) + coda_st_id = coda_st_obj.create(cr, uid, { 'name': statement['name'], 'type': statement['type'], 'coda_bank_account_id': statement['coda_bank_account_id'], - 'currency': statement['currency_id'], + 'currency': statement['currency_id'], 'journal_id': statement['journal_id'], 'coda_id': coda_id, 'date': statement['date'], + 'coda_creation_date': statement['coda_creation_date'], 'period_id': statement['period_id'], + 'old_balance_date': statement['old_balance_date'], + 'new_balance_date': statement['new_balance_date'], 'balance_start': statement['balance_start'], 'balance_end_real': statement['balance_end_real'], - 'state':'draft', }) coda_st_ids.append(coda_st_id) - if statement['type'] == 'normal': + # check duplicates for CODA's of type 'normal' + discard = False + if statement['type'] == 'normal' and statement['discard_dup']: + dup_ids = bank_st_obj.search(cr, uid, [('name', '=', statement['name']), ('company_id', '=', company_id)]) + if dup_ids: + discard = True + statement['type'] = 'info' # don't create a bank statement for duplicates + statement['coda_parsing_note'] += _("\n\nThe Bank Statement %s already exists, hence no duplicate Bank Statement has been created." \ + "\nSelect the 'CODA Bank Statement' to check the contents of %s.") \ + %(statement['name'], statement['name']) + + # create only bank statement for CODA's of type 'normal' + if statement['type'] == 'normal' and not discard: context.update({'ebanking_import': 1}) journal = journal_obj.browse(cr, uid, statement['journal_id'], context=context) + balance_start_check_date = statement['first_transaction_date'] or statement['date'] cr.execute('SELECT balance_end_real \ FROM account_bank_statement \ WHERE journal_id = %s and date < %s \ - ORDER BY date DESC,id DESC LIMIT 1', (statement['journal_id'], statement['date'])) + ORDER BY date DESC,id DESC LIMIT 1', (statement['journal_id'], balance_start_check_date)) res = cr.fetchone() balance_start_check = res and res[0] if balance_start_check == None: if journal.default_debit_account_id and (journal.default_credit_account_id == journal.default_debit_account_id): balance_start_check = journal.default_debit_account_id.balance else: - nb_err += 1 - err_string += _('\nConfiguration Error!\nPlease verify the Default Debit and Credit Account settings in journal %s.') % journal.name + nb_err += 1 + err_string += _('\nConfiguration Error in journal %s!' \ + '\nPlease verify the Default Debit and Credit Account settings.') % journal.name break - if balance_start_check <> statement['balance_start']: - nb_err += 1 - err_string += _('\nThe CODA Statement %s Starting Balance (%.2f) does not correspond with the previous Closing Balance (%.2f) in journal %s.') \ - % (statement['name'], statement['balance_start'], balance_start_check, journal.name) + if balance_start_check <> statement['balance_start']: + balance_start_err_string = _('\nThe CODA Statement %s Starting Balance (%.2f) does not correspond with the previous Closing Balance (%.2f) in journal %s!') \ + % (statement['name'], statement['balance_start'], balance_start_check, journal.name) + if statement['balance_start_enforce']: + nb_err += 1 + err_string += balance_start_err_string break + else: + statement['coda_parsing_note'] += '\n' + balance_start_err_string - bk_st_id = bank_st_obj.create(cr, uid, { + st_vals = { 'name': statement['name'], 'journal_id': statement['journal_id'], 'coda_statement_id': coda_st_id, - 'date': statement['date'], + 'date': statement['new_balance_date'], 'period_id': statement['period_id'], 'balance_start': statement['balance_start'], 'balance_end_real': statement['balance_end_real'], 'state': 'draft', - }) + } + st_hook_error, st_vals = self._statement_hook(cr, uid, statement['coda_bank_params'], st_vals, context=context) + if st_hook_error: + nb_err += 1 + err_string += st_hook_error + break + bk_st_id = bank_st_obj.create(cr, uid, st_vals, context=context) bk_st_ids.append(bk_st_id) coda_st_obj.write(cr, uid, [coda_st_id], {'statement_id': bk_st_id}, context=context) - - glob_id_stack = [(0, '', 0, '')] # stack with tuples (glob_lvl_flag, glob_code, glob_id, glob_name) - lines = statement['coda_statement_lines'] + st_line_seq = 0 for x in lines: line = lines[x] + + if not line['type'] == 'communication': + if line['trans_family'] in st_line_name_families: + line['name'] = get_st_line_name(line, context) + if line['type'] == 'information': + if line['struct_comm_type'] in parse_comms_info: + line['name'], line['communication'] = parse_comm_info(self, cr, uid, line, comm_type_table, context) + elif line['struct_comm_type'] in parse_comms_move: + line['name'], line['communication'] = parse_comm_move(self, cr, uid, line, comm_type_table, context) + elif line['struct_comm_type'] in parse_comms_move: + line['name'], line['communication'] = parse_comm_move(self, cr, uid, line, comm_type_table, context) # handling non-transactional records : line['type'] in ['information', 'communication'] - + if line['type'] == 'information': line['globalisation_id'] = glob_id_stack[-1][2] @@ -674,25 +990,25 @@ class account_coda_import(osv.osv_memory): line['trans_category'], line['trans_category_desc'], line['struct_comm_type'], line['struct_comm_type_desc'], line['communication']) - + coda_st_line_id = coda_st_line_obj.create(cr, uid, { 'sequence': line['sequence'], - 'ref': line['ref'], + 'ref': line['ref'], 'name': line['name'].strip() or '/', - 'type' : 'information', - 'date': line['entry_date'], + 'type' : 'information', + 'date': line['entry_date'], 'statement_id': coda_st_id, 'note': line_note, }) - + elif line['type'] == 'communication': line_note = _('Free Communication:\n %s') \ %(line['communication']) - + coda_st_line_id = coda_st_line_obj.create(cr, uid, { 'sequence': line['sequence'], - 'ref': line['ref'], + 'ref': line['ref'], 'name': line['name'].strip() or '/', 'type' : 'communication', 'date': statement['date'], @@ -700,34 +1016,39 @@ class account_coda_import(osv.osv_memory): 'note': line_note, }) - # handling transactional records, # line['type'] in ['globalisation', 'general', 'supplier', 'customer'] + # handling transactional records, # line['type'] in ['globalisation', 'general', 'supplier', 'customer'] else: - + + if line['ref_move_detail'] == '0000': + glob_id_stack = [(0, '', 0, '')] # initialise stack with tuples (glob_lvl_flag, glob_code, glob_id, glob_name) + glob_lvl_flag = line['glob_lvl_flag'] - if glob_lvl_flag: - if glob_id_stack[-1][0] == glob_lvl_flag: + if glob_lvl_flag: + if glob_id_stack[-1][0] == glob_lvl_flag: line['globalisation_id'] = glob_id_stack[-1][2] glob_id_stack.pop() else: glob_name = line['name'].strip() or '/' glob_code = seq_obj.get(cr, uid, 'statement.line.global') glob_id = glob_obj.create(cr, uid, { - 'code': glob_code, + 'code': glob_code, 'name': glob_name, 'type': 'coda', 'parent_id': glob_id_stack[-1][2], 'amount': line['globalisation_amount'], + 'payment_reference': line['payment_reference'] }) line['globalisation_id'] = glob_id glob_id_stack.append((glob_lvl_flag, glob_code, glob_id, glob_name)) - - line_note = _('Partner name: %s \nPartner Account Number: %s' \ + + line_note = _('Partner Name: %s \nPartner Account Number: %s' \ '\nTransaction Type: %s - %s' \ '\nTransaction Family: %s - %s' \ '\nTransaction Code: %s - %s' \ '\nTransaction Category: %s - %s' \ '\nStructured Communication Type: %s - %s' \ + '\nPayment Reference: %s' \ '\nCommunication: %s') \ %(line['counterparty_name'], line['counterparty_number'], line['trans_type'], line['trans_type_desc'], @@ -735,135 +1056,208 @@ class account_coda_import(osv.osv_memory): line['trans_code'], line['trans_code_desc'], line['trans_category'], line['trans_category_desc'], line['struct_comm_type'], line['struct_comm_type_desc'], + line['payment_reference'], line['communication']) - + if line['type'] == 'globalisation': - + coda_st_line_id = coda_st_line_obj.create(cr, uid, { 'sequence': line['sequence'], - 'ref': line['ref'], + 'ref': line['ref'], 'name': line['name'].strip() or '/', 'type' : 'globalisation', - 'val_date' : line['val_date'], + 'val_date' : line['val_date'], 'date': line['entry_date'], - 'globalisation_level': line['glob_lvl_flag'], - 'globalisation_amount': line['globalisation_amount'], - 'globalisation_id': line['globalisation_id'], + 'globalisation_level': line['glob_lvl_flag'], + 'globalisation_amount': line['globalisation_amount'], + 'globalisation_id': line['globalisation_id'], 'partner_id': line['partner_id'] or 0, 'account_id': line['account_id'], + 'payment_reference': line['payment_reference'], 'statement_id': coda_st_id, 'note': line_note, }) - else: # line['type'] in ['general', 'supplier', 'customer'] + else: # line['type'] in ['general', 'supplier', 'customer'] - if glob_lvl_flag == 0: + if glob_lvl_flag == 0: line['globalisation_id'] = glob_id_stack[-1][2] - if not line['account_id']: + if not line['account_id']: line['account_id'] = awaiting_acc - + coda_st_line_id = coda_st_line_obj.create(cr, uid, { 'sequence': line['sequence'], - 'ref': line['ref'], - 'name': line['name'] or '/', + 'ref': line['ref'], + 'name': line['name'].strip() or '/', 'type' : line['type'], - 'val_date' : line['val_date'], + 'val_date' : line['val_date'], 'date': line['entry_date'], 'amount': line['amount'], 'partner_id': line['partner_id'] or 0, 'counterparty_name': line['counterparty_name'], - 'counterparty_bic': line['counterparty_bic'], - 'counterparty_number': line['counterparty_number'], - 'counterparty_currency': line['counterparty_currency'], + 'counterparty_bic': line['counterparty_bic'], + 'counterparty_number': line['counterparty_number'], + 'counterparty_currency': line['counterparty_currency'], 'account_id': line['account_id'], - 'globalisation_level': line['glob_lvl_flag'], - 'globalisation_id': line['globalisation_id'], + 'globalisation_level': line['glob_lvl_flag'], + 'globalisation_id': line['globalisation_id'], + 'payment_reference': line['payment_reference'], 'statement_id': coda_st_id, 'note': line_note, }) if statement['type'] == 'normal': - - st_line_seq += 1 - voucher_id = False - line_name = line['name'].strip() - if not line_name: - if line['globalisation_id']: - line_name = glob_id_stack[-1][3] - else: - line_name = '/' - - if line['reconcile']: - voucher_vals = { - 'type': line['type'] == 'supplier' and 'payment' or 'receipt', - 'name': line_name, - 'partner_id': line['partner_id'], - 'journal_id': statement['journal_id'], - 'account_id': journal.default_credit_account_id.id, - 'company_id': journal.company_id.id, - 'currency_id': journal.company_id.currency_id.id, - 'date': line['entry_date'], - 'amount': abs(line['amount']), - 'period_id': statement['period_id'], - } - voucher_id = voucher_obj.create(cr, uid, voucher_vals, context=context) - - move_line = move_line_obj.browse(cr, uid, line['reconcile'], context=context) - voucher_dict = voucher_obj.onchange_partner_id(cr, uid, [], - partner_id = line['partner_id'], - journal_id = statement['journal_id'], - price = abs(line['amount']), - currency_id = journal.company_id.currency_id.id, - ttype = line['type'] == 'supplier' and 'payment' or 'receipt', - date = line['val_date'], - context = context) - #_logger.warning('voucher_dict = %s' % voucher_dict) - voucher_line_vals = False - if voucher_dict['value']['line_ids']: - for line_dict in voucher_dict['value']['line_ids']: - if line_dict['move_line_id'] == move_line.id: - voucher_line_vals = line_dict - if voucher_line_vals: - voucher_line_vals.update({ - 'voucher_id': voucher_id, + if line['amount'] != 0.0: + st_line_seq += 1 + voucher_id = False + line_name = line['name'].strip() + if not line_name: + if line['globalisation_id']: + line_name = glob_id_stack[-1][3] or '/' + else: + line_name = '/' + + if line['reconcile']: + + move_line = move_line_obj.browse(cr, uid, line['reconcile'], context=context) + company_currency_id = journal.company_id.currency_id.id + currency_id = journal.currency.id + if currency_id and currency_id != company_currency_id: + multi_currency = True + else: + multi_currency = False + context_multi_currency = context.copy() + context_multi_currency.update({'date': line['entry_date']}) + + line_cr_ids = [] + line_dr_ids = [] + amount_remaining = abs(line['amount']) + + #for entry in entries: + if move_line.reconcile_partial_id: + amount_original = abs(move_line.amount_residual_currency) + else: + amount_original = move_line.credit or move_line.debit + if multi_currency: + if move_line.currency_id and move_line.amount_currency: + amount_original = abs(move_line.amount_currency) + else: + amount_original = currency_obj.compute(cr, uid, company_currency_id, currency_id, original_amount, context=context_multi_currency) + if amount_remaining > 0: + amount_voucher_line = min(amount_original, abs(amount_remaining)) + amount_remaining -= amount_voucher_line + else: + amount_voucher_line = 0.0 + voucher_line_vals = { + 'name': move_line.name, + 'account_id': move_line.account_id.id, + 'amount': amount_voucher_line, + 'reconcile': amount_voucher_line == amount_original, + 'type': move_line.credit and 'dr' or 'cr', + 'move_line_id': move_line.id, + } + if voucher_line_vals['type'] == 'cr': + line_cr_ids += [(0, 0, voucher_line_vals)] + else: + line_dr_ids += [(0, 0, voucher_line_vals)] + + voucher_vals = { + 'type': line['type'] == 'supplier' and 'payment' or 'receipt', + 'name': line_name, + 'date': line['val_date'], + 'journal_id': statement['journal_id'], + 'account_id': journal.default_credit_account_id.id, + 'line_cr_ids': line_cr_ids, + 'line_dr_ids': line_dr_ids, + 'pre_line': len(line_dr_ids) > 0 and True or False, + 'period_id': statement['period_id'], + 'currency_id': journal.currency and journal.currency.id or journal.company_id.currency_id.id, + 'company_id': journal.company_id.id, + 'state': 'draft', 'amount': abs(line['amount']), - }) - voucher_line_obj.create(cr, uid, voucher_line_vals, context=context) + 'reference': line['ref'], + 'number': statement['name'] + '/' + str(st_line_seq), + 'partner_id': line['partner_id'], + } + + #_logger.warn('voucher_vals=%s, context=%s', voucher_vals, context) + context['journal_id'] = journal.id # add journal to context for __get_payment_rate_currency method + if line_cr_ids or line_dr_ids: + voucher_id = voucher_obj.create(cr, uid, voucher_vals, context=context) + else: + raise osv.except_osv(_('Error!'), _("Reconcile error while processing line with ref '%s' ! " \ + "\nPlease report this issue via your OpenERP support channel.") % line['ref']) + + # override default account mapping by mappings defined in rules engine + if statement['account_mapping_ids']: + kwargs = { + 'coda_bank_account_id': statement['coda_bank_account_id'], + 'trans_type_id': line['trans_type_id'], + 'trans_family_id': line['trans_family_id'], + 'trans_code_id': line['trans_code_id'], + 'trans_category_id': line['trans_category_id'], + 'struct_comm_type_id': line['struct_comm_type_id'], + 'partner_id': line['partner_id'], + 'context': context, + } + account_id = account_mapping_obj.account_id_get(cr, uid, **kwargs) + if account_id: + line['account_id'] = account_id + + st_line_vals = { + 'ref': line['ref'], + 'name': line_name, + 'type' : line['type'], + 'val_date' : line['val_date'], + 'date': line['entry_date'], + 'amount': line['amount'], + 'partner_id': line['partner_id'] or 0, + 'counterparty_name': line['counterparty_name'], + 'counterparty_bic': line['counterparty_bic'], + 'counterparty_number': line['counterparty_number'], + 'counterparty_currency': line['counterparty_currency'], + 'account_id': line['account_id'], + 'globalisation_id': line['globalisation_id'], + 'payment_reference': line['payment_reference'], + 'statement_id': bk_st_id, + 'voucher_id': voucher_id, + 'note': line_note, + } + + st_lines_vals = self._st_line_hook(cr, uid, statement['coda_bank_params'], st_line_vals, context=context) + if not st_lines_vals: + # removal of lines by the _coda_parsing_hook currently not supported : + # this qould require resequencing/renaming of linked vouchers & moves as well as removal of associated voucher/moves + err_string += _('\nError in _st_line_hook while processing CODA Statement %s for Bank Account %s!') \ + % (statement['coda_seq_number'], (statement['acc_number'] + ' (' + statement['currency'] + ') - ' + statement['description'])) + nb_err += 1 + for st_line_vals in st_lines_vals: + # To DO : update sequence of statement lines and naming of associated vouchers/moves when the hook returns multiple lines + st_line_vals.update({'sequence': st_line_seq}) + bank_st_line_id = bank_st_line_obj.create(cr, uid, st_line_vals, context=context) + if st_line_vals['voucher_id']: + voucher = voucher_obj.browse(cr, uid, st_line_vals['voucher_id'], context=context) + move_line = move_line_obj.browse(cr, uid, line['reconcile'], context=context) + if line['amount'] == (line['amount'] > 0 and move_line.debit or -move_line.credit): + wf_service.trg_validate(uid, 'account.voucher', st_line_vals['voucher_id'], 'proforma_voucher', cr) + if voucher.move_id: + move_line_obj.write(cr, uid, [x.id for x in voucher.move_ids], {'statement_id': bk_st_id}, context=context) - bank_st_line_id = bank_st_line_obj.create(cr, uid, { - 'sequence': st_line_seq, - 'ref': line['ref'], - 'name': line_name, - 'type' : line['type'], - 'val_date' : line['val_date'], - 'date': line['entry_date'], - 'amount': line['amount'], - 'partner_id': line['partner_id'] or 0, - 'counterparty_name': line['counterparty_name'], - 'counterparty_bic': line['counterparty_bic'], - 'counterparty_number': line['counterparty_number'], - 'counterparty_currency': line['counterparty_currency'], - 'account_id': line['account_id'], - 'globalisation_id': line['globalisation_id'], - 'statement_id': bk_st_id, - 'voucher_id': voucher_id, - 'note': line_note, - }) # end 'for x in lines' coda_st_obj.write(cr, uid, [coda_st_id], {}, context=context) # calculate balance st_balance = coda_st_obj.read(cr, uid, coda_st_id, ['balance_end', 'balance_end_real'], context=context) if st_balance['balance_end'] <> st_balance['balance_end_real']: - err_string += _('\nIncorrect ending Balance in CODA Statement %s for Bank Account %s.') \ + err_string += _('\nIncorrect ending Balance in CODA Statement %s for Bank Account %s!') \ % (statement['coda_seq_number'], (statement['acc_number'] + ' (' + statement['currency'] + ') - ' + statement['description'])) if statement['type'] == 'normal': nb_err += 1 break else: statement['coda_parsing_note'] += '\n' + err_string - - if statement['type'] == 'normal': - bank_st_obj.button_dummy(cr, uid, [bk_st_id], context=context) # calculate balance + + if statement['type'] == 'normal': + bank_st_obj.button_dummy(cr, uid, [bk_st_id], context=context) # calculate balance journal_name = journal.name else: journal_name = _('None') @@ -879,33 +1273,38 @@ class account_coda_import(osv.osv_memory): %(journal_name, coda_version, statement['coda_seq_number'], - statement['paper_seq_number'], + statement['paper_nb_seq_number'], (statement['acc_number'] + ' (' + statement['currency'] + ') - ' + statement['description']), statement['acc_holder'], statement['date'], float(statement['balance_start']), float(statement['balance_end_real']), - statement['coda_parsing_note']) + statement['coda_parsing_note'] % {'name':coda_statement['name']}) + if statement.get('separate_application') != '00000': + coda_note += _('\nCode Separate Application: %s') %statement['separate_application'] \ except osv.except_osv, e: cr.rollback() nb_err += 1 err_string += _('\nError ! ') + str(e) tb = ''.join(format_exception(*exc_info())) - _logger.error('Application Error while processing Statement %s\n%s' % (statement.get('name', '/'),tb)) + _logger.error('Application Error while processing Statement %s\n%s', + statement.get('name', '/'), tb) except Exception, e: cr.rollback() nb_err += 1 err_string += _('\nSystem Error : ') + str(e) tb = ''.join(format_exception(*exc_info())) - _logger.error('System Error while processing Statement %s\n%s' % (statement.get('name', '/'),tb)) + _logger.error('System Error while processing Statement %s\n%s', + statement.get('name', '/'),tb) except : cr.rollback() nb_err += 1 err_string = _('\nUnknown Error : ') + str(e) tb = ''.join(format_exception(*exc_info())) - _logger.error('Unknown Error while processing Statement %s\n%s' % (statement.get('name', '/'),tb)) + _logger.error('Unknown Error while processing Statement %s\n%s', + statement.get('name', '/'),tb) # end 'for statement in coda_statements' - + coda_note_header = _('CODA File is Imported :') coda_note_footer = _('\n\nNumber of statements : ') + str(len(coda_st_ids)) err_log = err_log + _('\nNumber of errors : ') + str(nb_err) + '\n' @@ -921,13 +1320,13 @@ class account_coda_import(osv.osv_memory): if batch: err_code = 'G0002' return (err_code, err_string) - raise osv.except_osv(_('CODA Import failed.'), err_string) - + raise osv.except_osv(_('CODA Import failed !'), err_string) + context.update({ 'bk_st_ids': bk_st_ids}) model_data_ids = mod_obj.search(cr, uid, [('model', '=', 'ir.ui.view'), ('name', '=', 'account_coda_import_result_view')], context=context) resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id'] self.write(cr, uid, ids, {'note': note}, context=context) - + return { 'name': _('Import CODA File result'), 'res_id': ids[0], @@ -941,10 +1340,26 @@ class account_coda_import(osv.osv_memory): 'type': 'ir.actions.act_window', } + def _statement_hook(self, cr, uid, coda_bank_params, st_vals, context=None): + """ + Use this method to take customer specific actions based upon the bank statement data. + Returns: + st_hook_error : False or string with error message + st_vals + """ + return False, st_vals + + def _st_line_hook(self, cr, uid, coda_bank_params, st_line_vals, context=None): + """ + Use this method to adapt the statement line created by the + CODA parsing to customer specific needs. + """ + return [st_line_vals] + def action_open_coda_statements(self, cr, uid, ids, context=None): if context is None: context = {} - module, xml_id = 'l10n_be_coda', 'action_coda_bank_statements' + module, xml_id = 'account_coda', 'action_coda_bank_statements' res_model, res_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, module, xml_id) action = self.pool.get('ir.actions.act_window').read(cr, uid, res_id, context=context) domain = eval(action.get('domain') or '[]') @@ -962,11 +1377,21 @@ class account_coda_import(osv.osv_memory): domain += [('id','in', context.get('bk_st_ids', False))] action.update({'domain': domain}) return action - + account_coda_import() +def repl_special(s): + s = s.replace("\'", "\'" + "'") + return s + def str2date(date_str): - return time.strftime('%Y-%m-%d', time.strptime(date_str,'%d%m%y')) + try: + return time.strftime('%Y-%m-%d', time.strptime(date_str,'%d%m%y')) + except: + return False + +def str2time(time_str): + return time_str[:2] + ':' + time_str[2:] def str2float(str): try: @@ -975,9 +1400,517 @@ def str2float(str): return 0.0 def list2float(lst): - try: - return str2float((lambda s : s[:-3] + '.' + s[-3:])(lst)) - except: - return 0.0 + try: + return str2float((lambda s : s[:-3] + '.' + s[-3:])(lst)) + except: + return 0.0 -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: +def number2float(s, d): + try: + return float(s[:len(s)-d] + '.' + s[len(s)-d:]) + except: + return False + +def _get_acc_numbers(acc_number): + #TODO this method is needed because the iban and bank account fields have been merged together. But sometimes we + # need to retrieve the normal bank account from the IBAN. This should be globalized and defined as a method on the + # bank account class. Each country part of the IBAN area should define its own code to do so. + acc_number = acc_number.replace(' ', '') + if acc_number.lower().startswith('be'): + return [acc_number[4:], acc_number] + return [acc_number] + +from base_iban.base_iban import _ref_iban, _format_iban + +def calc_iban_checksum(country, bban): + bban = bban.replace(' ', '').upper() + country.upper() + '00' + base = '' + for c in bban: + if c.isdigit(): + base += c + else: + base += str(ord(c) - ord('A') + 10) + kk = 98 - int(base) % 97 + return str(kk).rjust(2, '0') + +def check_bban(country, bban): + if country == 'BE': + try: + int(bban) + except: + return False + if len(bban) != 12: + return False + return True + +def check_iban(iban): + """ + Check the IBAN number (logic partially based upon base_iban module, cf. is_iban_valid method) + """ + iban = _format_iban(iban).lower() + if iban[:2] not in _ref_iban: + return False + if len(iban) != len(_format_iban(_ref_iban[iban[:2]])): + return False + #the four first digits have to be shifted to the end + iban = iban[4:] + iban[:4] + #letters have to be transformed into numbers (a = 10, b = 11, ...) + iban2 = "" + for char in iban: + if char.isalpha(): + iban2 += str(ord(char)-87) + else: + iban2 += char + #iban is correct if modulo 97 == 1 + return int(iban2) % 97 == 1 + +def get_bank(self, cr, uid, bic, iban): + + country_obj = self.pool.get('res.country') + bank_obj = self.pool.get('res.bank') + + bank_id = False + bank_name = False + feedback = False + bank_country = iban[:2] + try: + bank_country_id = country_obj.search(cr, uid, [('code', '=', bank_country)])[0] + except: + feedback = _("\n Bank lookup failed due to missing Country definition for Country Code '%s' !") \ + % (bank_country) + else: + if iban[:2] == 'BE' and 'code' in bank_obj.fields_get_keys(cr, uid): + # To DO : extend for other countries + bank_code = iban[4:7] + if bic: + bank_ids = bank_obj.search(cr, uid, [('bic', '=', bic), ('code', '=', bank_code), ('country', '=', bank_country_id)]) + if bank_ids: + bank_id = bank_ids[0] + else: + bank_id = bank_obj.create(cr, uid, { + 'name': bic, + 'code': bank_code, + 'bic': bic, + 'country': bank_country_id, + }) + else: + bank_ids = bank_obj.search(cr, uid, [('code', '=', bank_code), ('country', '=', bank_country_id)]) + if bank_ids: + bank_id = bank_ids[0] + bank_data = bank_obj.read(cr, uid, bank_id, fields=['bic', 'name']) + bic = bank_data['bic'] + bank_name = bank_data['name'] + else: + country = country_obj.browse(cr, uid, bank_country_id) + feedback = _("\n Bank lookup failed. Please define a Bank with Code '%s' and Country '%s' !") \ + % (bank_code, country.name) + else: + if not bic: + feedback = _("\n Bank lookup failed due to missing BIC in Bank Statement for IBAN '%s' !") \ + % (iban) + else: + bank_ids = bank_obj.search(cr, uid, [('bic', '=', bic), ('country', '=', bank_country_id)]) + if not bank_ids: + bank_name = bic + bank_id = bank_obj.create(cr, uid, { + 'name': bank_name, + 'bic': bic, + 'country': bank_country_id, + }) + else: + bank_id = bank_ids[0] + + return bank_id, bic, bank_name, feedback + +def update_partner_bank(self, cr, uid, bic, iban, partner_id, counterparty_name): + partner_bank_obj = self.pool.get('res.partner.bank') + bank_id = False + feedback = False + if check_iban(iban): + bank_id, bic, bank_name, feedback = get_bank(self, cr, uid, bic, iban) + if not bank_id: + return feedback + else: + bban = iban + #convert belgian BBAN numbers to IBAN + if check_bban('BE', iban): + kk = calc_iban_checksum('BE', iban) + iban = 'BE' + kk + iban + bank_id, bic, bank_name, feedback = get_bank(self, cr, uid, bic, iban) + if not bank_id: + return feedback + + if bank_id: + partner_bank_id = partner_bank_obj.create(cr, uid, { + 'partner_id': partner_id, + 'name': counterparty_name, + 'bank': bank_id, + 'state': 'iban', + 'bank_bic': bic, + 'bank_name': bank_name, + 'acc_number': iban, + }) + return feedback + +indent = '\n' + 8*' ' +st_line_name_families = ['13', '35', '41', '80'] +parse_comms_move = ['100', '101', '102', '103', '105', '106', '107', '108', '111', '113', '114', '115', '121', '122', '123', '124', '125', '126', '127'] +parse_comms_info = ['001', '002', '004', '005', '006', '007', '107', '008', '009', '010', '011'] + +def get_st_line_name(line, context): + #_logger.warn('get_st_line_name - entry').warn('ref = %s, family = %s, code = %s, categ = %s', line['ref'], line['trans_family'], line['trans_code'], line['trans_category']) + st_line_name = line['name'] + + if line['trans_family'] == '35' and line['trans_code'] in ['01', '37']: + st_line_name = ', '.join([_('Closing'), line['trans_code_desc'], line['trans_category_desc']]) + + if line['trans_family'] in ['13', '41']: + st_line_name = ', '.join([line['trans_family_desc'], line['trans_code_desc'], line['trans_category_desc']]) + + if line['trans_family'] in ['80']: + st_line_name = ', '.join([line['trans_code_desc'], line['trans_category_desc']]) + + return st_line_name + +def parse_comm_move(self, cr, uid, line, comm_type_table, context): + #_logger.warn('parse_comm_move - entry, ref = %s, family = %s, code = %s, categ = %s, comm_type = %s, name = %s', line['ref'], line['trans_family'], line['trans_code'], line['trans_category'], line['struct_comm_type'], line['name']) + comm_type = line['struct_comm_type'] + comm = st_line_comm = line['communication'] + st_line_name = line['name'] + + if comm_type in ['115', '121', '122', '126']: + _logger.warn('The parsing of Structured Commmunication Type %s has not yet been implemented. ' \ + 'Please contact Noviat (info@noviat.be) for more information about the development roadmap', comm_type) + + if comm_type == '100': + st_line_name = _('Payment with ISO 11649 structured format communication') + st_line_comm = '\n' + indent + _('Payment with a structured format communication applying the ISO standard 11649') + ':' + st_line_comm += indent + _('Structured creditor reference to remittance information') + st_line_comm += indent + comm + + if comm_type in ['101', '102']: + st_line_name = st_line_comm = '+++' + comm[0:3] + '/' + comm[3:7] + '/' + comm[7:12] + '+++' + + if comm_type == '103': + st_line_name = ', '.join([line['trans_family_desc'], _('Number')]) + st_line_comm = comm + + if comm_type == '105': + st_line_name = filter(lambda x: comm_type == x['code'], comm_type_table)[0]['description'] + amount_1 = list2float(comm[0:15]) + amount_2 = list2float(comm[15:30]) + rate = number2float(comm[30:42], 8) + currency = comm[42:45] + struct_format_comm = comm[45:57].strip() + country_code = comm[57:59] + amount_3 = list2float(comm[59:74]) + st_line_comm = '\n' + indent + st_line_name + indent + _('Gross amount in the currency of the account') + ': %.2f' % amount_1 + st_line_comm += indent + _('Gross amount in the original currency') + ': %.2f' % amount_2 + st_line_comm += indent + _('Rate') + ': %.4f' % rate + st_line_comm += indent + _('Currency') + ': %s' % currency + st_line_comm += indent + _('Structured format communication') + ': %s' % struct_format_comm + st_line_comm += indent + _('Country code of the principal') + ': %s' % country_code + st_line_comm += indent + _('Equivalent in EUR') + ': %.2f' % amount_3 + + if comm_type == '106': + if not st_line_name and line['trans_family'] not in st_line_name_families: + st_line_name = _('VAT, withholding tax on income, commission, etc.') + interest = comm[30:42].strip('0') + st_line_comm = '\n' + indent + st_line_name + indent + _('Equivalent in the currency of the account') + ': %.2f' % list2float(comm[0:15]) + st_line_comm += indent + _('Amount on which % is calculated') + ': %.2f' % list2float(comm[15:30]) + st_line_comm += indent + _('Percent') + ': %.4f' % number2float(comm[30:42], 8) + st_line_comm += indent + (comm[42] == 1 and _('Minimum applicable') or _('Minimum not applicable')) + st_line_comm += indent + _('Equivalent in EUR') + ': %.2f' % list2float(comm[43:58]) + + if comm_type == '107': + paid_refusals = { + '0': _('paid'), + '1': _('direct debit cancelled or nonexistent'), + '2': _('refusal - other reason'), + 'D': _('payer disagrees'), + 'E': _('direct debit number linked to another identification number of the creditor')} + st_line_name = _('Direct debit - DOM\'80') + direct_debit_number = comm[0:12].strip() + pivot_date = str2date(comm[12:18]) + comm_zone = comm[18:48] + paid_refusal = paid_refusals.get(comm[48], '') + creditor_number = comm[49:60].strip() + st_line_comm = '\n' + indent + st_line_name + indent + _('Direct Debit Number') + ': %s' % direct_debit_number + st_line_comm += indent + _('Central (Pivot) Date') + ': %s' % pivot_date + st_line_comm += indent + _('Communication Zone') + ': %s' % comm_zone + st_line_comm += indent + _('Paid or reason for refusal') + ': %s' % paid_refusal + st_line_comm += indent + _("Creditor's Number") + ': %s' % creditor_number + + if comm_type == '108': + st_line_name = _('Closing, period from %s to %s') % (str2date(comm[42:48]), str2date(comm[48:54])) + interest = comm[30:42].strip('0') + st_line_comm = '\n' + indent + st_line_name + indent + _('Equivalent in the currency of the account') + ': %.2f' % list2float(comm[0:15]) + if interest: + st_line_comm += indent + _('Interest rates, calculation basis') + ': %.2f' % list2float(comm[15:30]) + \ + indent + _('Interest') + ': %.2f' % list2float(comm[30:42]) + + if comm_type =='111': + card_schemes = { + '1': 'Bancontact/Mister Cash', + '2': _('Private'), + '3': 'Maestro', + '5': 'TINA', + '9': _('Other')} + trans_types = { + '1': _('Withdrawal'), + '2': _('Cumulative on network'), + '7': _('Distribution sector'), + '8': _('Teledata'), + '9': _('Fuel')} + st_line_name = _('POS credit - globalisation') + card_scheme = card_schemes.get(comm[0], '') + pos_number = comm[1:7].strip() + period_number = comm[7:10].strip() + first_sequence_number = comm[10:16].strip() + trans_first_date = str2date(comm[16:22]) + last_sequence_number = comm[22:28].strip() + trans_last_date = str2date(comm[28:34]) + trans_type = trans_types.get(comm[34], '') + terminal_name = comm[35:50].strip() + terminal_city = comm[51:60].strip() + st_line_comm = '\n' + indent + st_line_name + indent + _('Card Scheme') + ': %s' % card_scheme + st_line_comm += indent + _('POS Number') + ': %s' % pos_number + st_line_comm += indent + _('Period Number') + ': %s' % period_number + st_line_comm += indent + _('First Transaction Sequence Number') + ': %s' % first_sequence_number + st_line_comm += indent + _('Date of first transaction') + ': %s' % trans_first_date + st_line_comm += indent + _('Last Transaction Sequence Number') + ': %s' % last_sequence_number + st_line_comm += indent + _('Date of last transaction') + ': %s' % trans_last_date + st_line_comm += indent + _('Transaction Type') + ': %s' % trans_type + st_line_comm += indent + _('Terminal Identification') + ': %s' % terminal_name + ', ' + terminal_city + + if comm_type == '113': + card_schemes = { + '1': 'Bancontact/Mister Cash', + '2': 'Maestro', + '3': _('Private'), + '9': _('Other')} + trans_types = { + '1': _('Withdrawal'), + '2': _('Proton loading'), + '3': _('Reimbursement Proton balance'), + '4': _('Reversal of purchases'), + '7': _('Distribution sector'), + '8': _('Teledata'), + '9': _('Fuel')} + product_codes = { + '01': _('premium with lead substitute'), + '02': _('europremium'), + '03': _('diesel'), + '04': _('LPG'), + '06': _('premium plus 98 oct'), + '07': _('regular unleaded'), + '08': _('domestic fuel oil'), + '09': _('lubricants'), + '10': _('petrol'), + '11': _('premium 99+'), + '12': _('Avgas'), + '16': _('other types'), + } + st_line_name = _('ATM/POS debit') + card_number = comm[0:16].strip() + card_scheme = card_schemes.get(comm[16], '') + terminal_number = comm[17:23].strip() + sequence_number = comm[23:29].strip() + trans_date = comm[29:35].strip() and str2date(comm[29:35]) or '' + trans_hour = comm[35:39].strip() and str2time(comm[35:39]) or '' + trans_type = trans_types.get(comm[39], '') + terminal_name = comm[40:56].strip() + terminal_city = comm[56:66].strip() + orig_amount = comm[66:81].strip() and list2float(comm[66:81]) + rate = number2float(comm[81:93], 8) + currency = comm[93:96] + volume = number2float(comm[96:101], 2) + product_code = product_codes.get(comm[101:103], '') + unit_price = number2float(comm[103:108], 2) + st_line_comm = '\n' + indent + st_line_name + indent + _('Card Number') + ': %s' % card_number + st_line_comm += indent + _('Card Scheme') + ': %s' % card_scheme + if terminal_number: + st_line_comm += indent + _('Terminal Number') + ': %s' % terminal_number + st_line_comm += indent + _('Transaction Sequence Number') + ': %s' % sequence_number + st_line_comm += indent + _('Time') + ': %s' % trans_date + ' ' + trans_hour + st_line_comm += indent + _('Transaction Type') + ': %s' % trans_type + st_line_comm += indent + _('Terminal Identification') + ': %s' % terminal_name + ', ' + terminal_city + if orig_amount: + st_line_comm += indent + _('Original Amount') + ': %.2f' % orig_amount + st_line_comm += indent + _('Rate') + ': %.4f' % rate + st_line_comm += indent + _('Currency') + ': %s' % currency + if volume: + st_line_comm += indent + _('Volume') + ': %.2f' % volume + if product_code: + st_line_comm += indent + _('Product Code') + ': %s' % product_code + if unit_price: + st_line_comm += indent + _('Unit Price') + ': %.2f' % unit_price + + if comm_type == '114': + card_schemes = { + '1': 'Bancontact/Mister Cash', + '2': 'Maestro', + '3': _('Private'), + '5': 'TINA', + '9': _('Other')} + trans_types = { + '1': _('Withdrawal'), + '7': _('Distribution sector'), + '8': _('Teledata'), + '9': _('Fuel')} + st_line_name = _('POS credit - individual transaction') + card_scheme = card_schemes.get(comm[0], '') + pos_number = comm[1:7].strip() + period_number = comm[7:10].strip() + sequence_number = comm[10:16].strip() + trans_date = str2date(comm[16:22]) + trans_hour = str2time(comm[22:26]) + trans_type = trans_types.get(comm[26], '') + terminal_name = comm[27:43].strip() + terminal_city = comm[43:53].strip() + trans_reference = comm[53:69].strip() + st_line_comm = '\n' + indent + st_line_name + indent + _('Card Scheme') + ': %s' % card_scheme + st_line_comm += indent + _('POS Number') + ': %s' % pos_number + st_line_comm += indent + _('Period Number') + ': %s' % period_number + st_line_comm += indent + _('Transaction Sequence Number') + ': %s' % sequence_number + st_line_comm += indent + _('Time') + ': %s' % trans_date + ' ' + trans_hour + st_line_comm += indent + _('Transaction Type') + ': %s' % trans_type + st_line_comm += indent + _('Terminal Identification') + ': %s' % terminal_name + ', ' + terminal_city + st_line_comm += indent + _('Transaction Reference') + ': %s' % trans_reference + + if comm_type == '123': + starting_date = str2date(comm[0:6]) + maturity_date = comm[6:12] == '999999' and _('guarantee without fixed term') or str2date(comm[0:6]) + basic_amount = list2float(comm[12:27]) + percent = number2float(comm[27:39], 8) + term = comm[39:43].lstrip('0') + minimum = comm[43] == '1' and True or False + guarantee_number = comm[44:57].strip() + st_line_comm = '\n' + indent + st_line_name + indent + _('Starting Date') + ': %s' % starting_date + st_line_comm += indent + _('Maturity Date') + ': %s' % maturity_date + st_line_comm += indent + _('Basic Amount') + ': %.2f' % basic_amount + st_line_comm += indent + _('Percentage') + ': %.4f' % percent + st_line_comm += indent + _('Term in days') + ': %s' % term + st_line_comm += indent + (minimum and _('Minimum applicable') or _('Minimum not applicable')) + st_line_comm += indent + _('Guarantee Number') + ': %s' % guarantee_number + + if comm_type == '124': + card_issuers = { + '1': 'Mastercard', + '2': 'Visa', + '3': 'American Express', + '4': 'Diners Club', + '9': _('Other')} + st_line_name = _('Settlement credit cards') + card_number = comm[0:20].strip() + card_issuer = card_issuers.get(comm[20], '') + invoice_number = comm[21:33].strip() + identification_number = comm[33:48].strip() + date = comm[48:54].strip() and str2date(comm[48:54]) or '' + st_line_comm = '\n' + indent + st_line_name + indent + _('Card Number') + ': %s' % card_number + st_line_comm += indent + _('Issuing Institution') + ': %s' % card_issuer + st_line_comm += indent + _('Invoice Number') + ': %s' % invoice_number + st_line_comm += indent + _('Identification Number') + ': %s' % identification_number + st_line_comm += indent + _('Date') + ': %s' % date + + if comm_type == '125': + if line['trans_family'] not in st_line_name_families: + st_line_name = _('Credit') + credit_account = comm[0:27].strip() + if check_bban('BE', credit_account): + credit_account = '-'.join([credit_account[:3], credit_account[3:10], credit_account[10:]]) + old_balance = list2float(comm[27:42]) + new_balance = list2float(comm[42:57]) + amount = list2float(comm[57:72]) + currency = comm[72:75] + start_date = str2date(comm[75:81]) + end_date = str2date(comm[81:87]) + rate = number2float(comm[87:99], 8) + trans_reference = comm[99:112].strip() + st_line_comm = '\n' + indent + st_line_name + indent + _('Credit Account Number') + ': %s' % credit_account + st_line_comm += indent + _('Old Balance') + ': %.2f' % old_balance + st_line_comm += indent + _('New Balance') + ': %.2f' % new_balance + st_line_comm += indent + _('Amount') + ': %.2f' % amount + st_line_comm += indent + _('Currency') + ': %s' % currency + st_line_comm += indent + _('Starting Date') + ': %s' % start_date + st_line_comm += indent + _('End Date') + ': %s' % end_date + st_line_comm += indent + _('Nominal Interest Rate or Rate of Charge') + ': %.4f' % rate + st_line_comm += indent + _('Transaction Reference') + ': %s' % trans_reference + + if comm_type == '127': + direct_debit_types = { + '0': _('unspecified'), + '1': _('recurrent'), + '2': _('one-off'), + '3': _('1-st (recurrent)'), + '4': _('last (recurrent)')} + direct_debit_schemes = { + '0': _('unspecified'), + '1': _('SEPA core'), + '2': _('SEPA B2B')} + paid_refusals = { + '0': _('paid'), + '1': _('technical problem'), + '2': _('refusal - reason not specified'), + '3': _('debtor disagrees'), + '4': _('debtor\'s account problem')} + R_types = { + '0': _('paid'), + '1': _('reject'), + '2': _('return'), + '3': _('refund'), + '4': _('reversal'), + '5': _('cancellation')} + st_line_name = _('European direct debit (SEPA)') + settlement_date = str2date(comm[0:6]) + direct_debit_type = direct_debit_types.get(comm[6], '') + direct_debit_scheme = direct_debit_schemes.get(comm[7], '') + paid_refusal = paid_refusals.get(comm[8], '') + creditor_id = comm[9:44].strip() + mandate_ref = comm[44:79].strip() + comm_zone = comm[79:141] + R_type = R_types.get(comm[141], '') + reason = comm[142:146].strip() + st_line_comm = '\n' + indent + st_line_name + indent + _('Settlement_Date') + ': %s' % settlement_date + st_line_comm += indent + _('Direct Debit Type') + ': %s' % direct_debit_type + st_line_comm += indent + _('Direct Debit Scheme') + ': %s' % direct_debit_scheme + st_line_comm += indent + _('Paid or reason for refusal') + ': %s' % paid_refusal + st_line_comm += indent + _('Creditor\'s Identification Code') + ': %s' % creditor_id + st_line_comm += indent + _('Mandate Reference') + ': %s' % mandate_ref + st_line_comm += indent + _('Communication') + ': %s' % comm_zone + st_line_comm += indent + _('R transaction Type') + ': %s' % R_type + st_line_comm += indent + _('Reason') + ': %s' % reason + + return st_line_name, st_line_comm + +def parse_comm_info(self, cr, uid, line, comm_type_table, context): + comm_type = line['struct_comm_type'] + comm = st_line_comm = line['communication'] + st_line_name = line['name'] + + if comm_type == '001': + st_line_name = filter(lambda x: comm_type == x['code'], comm_type_table)[0]['description'] + st_line_comm = '\n' + indent + st_line_name + indent + _('Name') + ': %s' % comm[0:70].strip() + st_line_comm += indent + _('Street') + ': %s' % comm[70:105].strip() + st_line_comm += indent + _('Locality') + ': %s' % comm[105:140].strip() + st_line_comm += indent + _('Identification Code') + ': %s' % comm[140:175].strip() + + if comm_type in ['002', '004', '005']: + st_line_name = filter(lambda x: comm_type == x['code'], comm_type_table)[0]['description'] + st_line_comm = comm.strip() + + if comm_type == '006': + amount_sign = comm[48] + amount = (comm[48] == '1' and '-' or '') + ('%.2f' % list2float(comm[33:48])) + ' ' + comm[30:33] + st_line_name = filter(lambda x: comm_type == x['code'], comm_type_table)[0]['description'] + st_line_comm = '\n' + indent + st_line_name + indent + _('Description of the detail') + ': %s' % comm[0:30].strip() + st_line_comm += indent + _('Amount') + ': %s' % amount + st_line_comm += indent + _('Category') + ': %s' % comm[49:52].strip() + + if comm_type not in ['001', '002', '004', '005', '006']: + _logger.warn('The parsing of Structured Commmunication Type %s has not yet been implemented. ' \ + 'Please contact Noviat (info@noviat.be) for more information about the development roadmap', comm_type) + + return st_line_name, st_line_comm + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file