Merge pull request #30 from savoirfairelinux/7.0-pep8

7.0 pep8
This commit is contained in:
Guewen Baconnier
2014-09-09 14:02:08 +02:00
104 changed files with 2548 additions and 1899 deletions

View File

@@ -36,7 +36,7 @@ class AccountBankStatement(orm.Model):
@param defaults: dictionary of default move line values. Usually
the same as the originating move line.
return one or more serialized tax move lines and a set of values to
return one or more serialized tax move lines and a set of values to
update the originating move line with, containing the new amount.
"""
@@ -50,8 +50,8 @@ class AccountBankStatement(orm.Model):
fiscal_position = (
st_line.partner_id.property_account_position
if st_line.partner_id and
st_line.partner_id.property_account_position
if st_line.partner_id
and st_line.partner_id.property_account_position
else False)
tax_ids = self.pool.get('account.fiscal.position').map_tax(
cr, uid, fiscal_position, [st_line.tax_id])
@@ -66,25 +66,33 @@ class AccountBankStatement(orm.Model):
update_move_line['tax_code_id'] = tax['base_code_id']
update_move_line['tax_amount'] = tax['base_sign'] * (
computed_taxes.get('total', 0.0))
# As the tax is inclusive, we need to correct the amount on the
# original move line
# As the tax is inclusive, we need to correct the amount
# on the original move line
amount = computed_taxes.get('total', 0.0)
update_move_line['credit'] = ((amount < 0) and -amount) or 0.0
update_move_line['debit'] = ((amount > 0) and amount) or 0.0
update_move_line['credit'] = (
(amount < 0) and -amount) or 0.0
update_move_line['debit'] = (
(amount > 0) and amount) or 0.0
move_lines.append({
'move_id': defaults['move_id'],
'name': defaults.get('name', '') + ' ' + ustr(tax['name'] or ''),
'name': (
defaults.get('name', '')
+ ' ' + ustr(tax['name'] or '')),
'date': defaults.get('date', False),
'partner_id': defaults.get('partner_id', False),
'ref': defaults.get('ref', False),
'statement_id': defaults.get('statement_id'),
'tax_code_id': tax['tax_code_id'],
'tax_amount': tax['tax_sign'] * tax.get('amount', 0.0),
'account_id': tax.get('account_collected_id', defaults['account_id']),
'account_id': (
tax.get('account_collected_id',
defaults['account_id'])),
'credit': tax['amount'] < 0 and - tax['amount'] or 0.0,
'debit': tax['amount'] > 0 and tax['amount'] or 0.0,
'account_id': tax.get('account_collected_id', defaults['account_id']),
'account_id': (
tax.get('account_collected_id',
defaults['account_id'])),
})
return move_lines, update_move_line

View File

@@ -28,7 +28,7 @@ class AccountBankStatementLine(orm.Model):
_columns = {
'tax_id': fields.many2one(
'account.tax', 'Tax',
domain=[('price_include','=', True)],
domain=[('price_include', '=', True)],
help="Apply an (inclusive) tax from the bank statement line",
),
}

View File

@@ -11,8 +11,8 @@
# garantees and support are strongly adviced to contract EduSense BV
#
# 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
# 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,
@@ -24,6 +24,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import sepa
import record
import banking_import_transaction
@@ -33,5 +34,3 @@ import wizard
import res_partner
import res_bank
import res_partner_bank
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -70,7 +70,8 @@
+ IBAN accounts are the standard in the SEPA countries
+ local accounts are derived from SEPA (excluding Turkey) but are
considered to be identical to the corresponding SEPA account.
+ Banks are identified with either Country + Bank code + Branch code or BIC
+ Banks are identified with either Country + Bank code + Branch code or
BIC
+ Each bank can have its own pace in introducing SEPA into their
communication with their customers.
+ National online databases can be used to convert BBAN's to IBAN's.

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -86,11 +86,11 @@ class account_banking_account_settings(orm.Model):
string='Partner'),
'default_credit_account_id': fields.many2one(
'account.account', 'Default credit account', select=True,
help=('The account to use when an unexpected payment was signaled. '
'This can happen when a direct debit payment is cancelled '
help=('The account to use when an unexpected payment was signaled.'
' This can happen when a direct debit payment is cancelled '
'by a customer, or when no matching payment can be found. '
' Mind that you can correct movements before confirming them.'
),
'Mind that you can correct movements before confirming them.'
),
required=True
),
'default_debit_account_id': fields.many2one(
@@ -98,27 +98,27 @@ class account_banking_account_settings(orm.Model):
select=True, required=True,
help=('The account to use when an unexpected payment is received. '
'This can be needed when a customer pays in advance or when '
'no matching invoice can be found. Mind that you can correct '
'movements before confirming them.'
),
'no matching invoice can be found. Mind that you can '
'correct movements before confirming them.'
),
),
'costs_account_id': fields.many2one(
'account.account', 'Bank Costs Account', select=True,
help=('The account to use when the bank invoices its own costs. '
'Leave it blank to disable automatic invoice generation '
'on bank costs.'
),
),
),
'invoice_journal_id': fields.many2one(
'account.journal', 'Costs Journal',
'account.journal', 'Costs Journal',
help=('This is the journal used to create invoices for bank costs.'
),
),
),
'bank_partner_id': fields.many2one(
'res.partner', 'Bank Partner',
help=('The partner to use for bank costs. Banks are not partners '
'by default. You will most likely have to create one.'
),
),
),
}
@@ -134,7 +134,7 @@ class account_banking_account_settings(orm.Model):
return user['company_id'][0]
return self.pool.get('res.company').search(
cr, uid, [('parent_id', '=', False)])[0]
def _default_partner_id(self, cr, uid, context=None, company_id=False):
if not company_id:
company_id = self._default_company(cr, uid, context=context)
@@ -196,7 +196,7 @@ class account_banking_account_settings(orm.Model):
values['journal_id'] = bank['journal_id'][0]
return {'value': values}
def onchange_company_id (
def onchange_company_id(
self, cr, uid, ids, company_id=False, context=None):
if not company_id:
return {}
@@ -229,37 +229,59 @@ class account_banking_imported_file(orm.Model):
_description = __doc__
_rec_name = 'date'
_columns = {
'company_id': fields.many2one('res.company', 'Company',
select=True, readonly=True
),
'date': fields.datetime('Import Date', readonly=True, select=True,
states={'draft': [('readonly', False)]}
),
'format': fields.char('File Format', size=20, readonly=True,
states={'draft': [('readonly', False)]}
),
'file': fields.binary('Raw Data', readonly=True,
states={'draft': [('readonly', False)]}
),
'file_name': fields.char('File name', size=256),
'log': fields.text('Import Log', readonly=True,
states={'draft': [('readonly', False)]}
),
'user_id': fields.many2one('res.users', 'Responsible User',
readonly=True, select=True,
states={'draft': [('readonly', False)]}
),
'state': fields.selection(
[('unfinished', 'Unfinished'),
('error', 'Error'),
('review', 'Review'),
('ready', 'Finished'),
], 'State', select=True, readonly=True
'company_id': fields.many2one(
'res.company',
'Company',
select=True,
readonly=True,
),
'date': fields.datetime(
'Import Date',
readonly=True,
select=True,
states={'draft': [('readonly', False)]},
),
'format': fields.char(
'File Format',
size=20,
readonly=True,
states={'draft': [('readonly', False)]},
),
'file': fields.binary(
'Raw Data',
readonly=True,
states={'draft': [('readonly', False)]},
),
'file_name': fields.char('File name', size=256),
'log': fields.text(
'Import Log',
readonly=True,
states={'draft': [('readonly', False)]},
),
'user_id': fields.many2one(
'res.users',
'Responsible User',
readonly=True,
select=True,
states={'draft': [('readonly', False)]},
),
'state': fields.selection(
[
('unfinished', 'Unfinished'),
('error', 'Error'),
('review', 'Review'),
('ready', 'Finished'),
],
'State',
select=True,
readonly=True,
),
'statement_ids': fields.one2many(
'account.bank.statement',
'banking_id',
'Statements',
readonly=False,
),
'statement_ids': fields.one2many('account.bank.statement',
'banking_id', 'Statements',
readonly=False,
),
}
_defaults = {
'date': fields.date.context_today,
@@ -284,11 +306,17 @@ class account_bank_statement(orm.Model):
_inherit = 'account.bank.statement'
_columns = {
'period_id': fields.many2one('account.period', 'Period',
required=False, readonly=True),
'banking_id': fields.many2one('account.banking.imported.file',
'Imported File', readonly=True,
),
'period_id': fields.many2one(
'account.period',
'Period',
required=False,
readonly=True,
),
'banking_id': fields.many2one(
'account.banking.imported.file',
'Imported File',
readonly=True,
),
}
_defaults = {
@@ -316,12 +344,12 @@ class account_bank_statement(orm.Model):
statement.write({'period_id': line.period_id.id})
statement.refresh()
return True
# Redefine the constraint, or it still refer to the original method
_constraints = [
(_check_company_id,
'The journal and period chosen have to belong to the same company.',
['journal_id','period_id']),
['journal_id', 'period_id']),
]
def _get_period(self, cr, uid, date=False, context=None):
@@ -410,7 +438,7 @@ class account_bank_statement(orm.Model):
account_move_obj.post(
cr, uid, [st_line.voucher_id.move_id.id], context={})
else:
# Write stored reconcile_id and pay invoices through workflow
# Write stored reconcile_id and pay invoices through workflow
if st_line.reconcile_id:
move_ids = [move.id for move in st_line.move_ids]
torec = account_move_line_obj.search(
@@ -419,11 +447,12 @@ class account_bank_statement(orm.Model):
('account_id', '=', st_line.account_id.id)],
context=context)
account_move_line_obj.write(cr, uid, torec, {
(st_line.reconcile_id.line_partial_ids and
'reconcile_partial_id' or 'reconcile_id'):
st_line.reconcile_id.id }, context=context)
(st_line.reconcile_id.line_partial_ids
and 'reconcile_partial_id'
or 'reconcile_id'): st_line.reconcile_id.id
}, context=context)
for move_line in (st_line.reconcile_id.line_id or []) + (
st_line.reconcile_id.line_partial_ids or []):
st_line.reconcile_id.line_partial_ids or []):
netsvc.LocalService("workflow").trg_trigger(
uid, 'account.move.line', move_line.id, cr)
return res
@@ -438,7 +467,7 @@ class account_bank_statement(orm.Model):
if ids and isinstance(ids, (int, long)):
ids = [ids]
noname_ids = self.search(
cr, uid, [('id', 'in', ids),('name', '=', '/')],
cr, uid, [('id', 'in', ids), ('name', '=', '/')],
context=context)
for st in self.browse(cr, uid, noname_ids, context=context):
if st.journal_id.sequence_id:
@@ -451,7 +480,7 @@ class account_bank_statement(orm.Model):
cr, uid, st.journal_id.sequence_id.id, context=c)
self.write(
cr, uid, ids, {'name': st_number}, context=context)
return super(account_bank_statement, self).button_confirm_bank(
cr, uid, ids, context)
@@ -464,7 +493,7 @@ class account_voucher(orm.Model):
context = {}
if not context.get('period_id') and context.get('move_line_ids'):
move_line = self.pool.get('account.move.line').browse(
cr, uid , context.get('move_line_ids')[0], context=context)
cr, uid, context.get('move_line_ids')[0], context=context)
return move_line.period_id.id
return super(account_voucher, self)._get_period(cr, uid, context)
@@ -498,8 +527,8 @@ class account_bank_statement_line(orm.Model):
which is inaccessible from within this method.
'''
res_users_obj = self.pool.get('res.users')
return res_users_obj.browse(cr, uid, uid,
context=context).company_id.currency_id.id
return res_users_obj.browse(
cr, uid, uid, context=context).company_id.currency_id.id
def _get_invoice_id(self, cr, uid, ids, name, args, context=None):
res = {}
@@ -509,7 +538,7 @@ class account_bank_statement_line(orm.Model):
st_line.reconcile_id and
(st_line.reconcile_id.line_id or
st_line.reconcile_id.line_partial_ids) or
st_line.import_transaction_id and
st_line.import_transaction_id and
st_line.import_transaction_id.move_line_id and
[st_line.import_transaction_id.move_line_id] or []):
if move_line.invoice:
@@ -519,36 +548,70 @@ class account_bank_statement_line(orm.Model):
_columns = {
# Redefines. Todo: refactor away to view attrs
'amount': fields.float('Amount', readonly=True,
digits_compute=dp.get_precision('Account'),
states={'draft': [('readonly', False)]}),
'ref': fields.char('Ref.', size=32, readonly=True,
states={'draft': [('readonly', False)]}),
'name': fields.char('Name', size=64, required=False, readonly=True,
states={'draft': [('readonly', False)]}),
'date': fields.date('Date', required=True, readonly=True,
states={'draft': [('readonly', False)]}),
'amount': fields.float(
'Amount',
readonly=True,
digits_compute=dp.get_precision('Account'),
states={'draft': [('readonly', False)]},
),
'ref': fields.char(
'Ref.',
size=32,
readonly=True,
states={'draft': [('readonly', False)]},
),
'name': fields.char(
'Name',
size=64,
required=False,
readonly=True,
states={'draft': [('readonly', False)]},
),
'date': fields.date(
'Date',
required=True,
readonly=True,
states={'draft': [('readonly', False)]},
),
# New columns
'trans': fields.char('Bank Transaction ID', size=15, required=False,
readonly=True,
states={'draft':[('readonly', False)]},
),
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
required=False, readonly=True,
states={'draft':[('readonly', False)]},
),
'period_id': fields.many2one('account.period', 'Period', required=True,
states={'confirmed': [('readonly', True)]}),
'currency': fields.many2one('res.currency', 'Currency', required=True,
states={'confirmed': [('readonly', True)]}),
'trans': fields.char(
'Bank Transaction ID',
size=15,
required=False,
readonly=True,
states={'draft': [('readonly', False)]},
),
'partner_bank_id': fields.many2one(
'res.partner.bank',
'Bank Account',
required=False,
readonly=True,
states={'draft': [('readonly', False)]},
),
'period_id': fields.many2one(
'account.period',
'Period',
required=True,
states={'confirmed': [('readonly', True)]},
),
'currency': fields.many2one(
'res.currency',
'Currency',
required=True,
states={'confirmed': [('readonly', True)]},
),
'reconcile_id': fields.many2one(
'account.move.reconcile', 'Reconciliation', readonly=True
),
'account.move.reconcile',
'Reconciliation',
readonly=True,
),
'invoice_id': fields.function(
_get_invoice_id, method=True, string='Linked Invoice',
type='many2one', relation='account.invoice'
),
_get_invoice_id,
method=True,
string='Linked Invoice',
type='many2one',
relation='account.invoice',
),
}
_defaults = {
@@ -577,9 +640,9 @@ class invoice(orm.Model):
_inherit = 'account.invoice'
def test_undo_paid(self, cr, uid, ids, context=None):
"""
"""
Called from the workflow. Used to unset paid state on
invoices that were paid with bank transfers which are being cancelled
invoices that were paid with bank transfers which are being cancelled
"""
for invoice in self.read(cr, uid, ids, ['reconciled'], context):
if invoice['reconciled']:
@@ -592,22 +655,20 @@ class invoice(orm.Model):
'''
return [('none', _('Free Reference')),
('structured', _('Structured Reference')),
]
]
_columns = {
'reference_type': fields.selection(_get_reference_type,
'Reference Type', required=True
)
)
}
invoice()
class account_move_line(orm.Model):
_inherit = "account.move.line"
def get_balance(self, cr, uid, ids, context=None):
"""
"""
Return the balance of any set of move lines.
Not to be confused with the 'balance' field on this model, which
@@ -617,9 +678,6 @@ class account_move_line(orm.Model):
if not ids:
return total
for line in self.read(
cr, uid, ids, ['debit', 'credit'], context=context):
cr, uid, ids, ['debit', 'credit'], context=context):
total += (line['debit'] or 0.0) - (line['credit'] or 0.0)
return total
account_move_line()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,8 @@
# Copyright (C) 2011 Therp BV (<http://therp.nl>)
#
# 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
# 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,
@@ -24,10 +24,11 @@ __name__ = ("account.bank.statement.line:: set new field 'state' to "
"confirmed for all statement lines belonging to confirmed "
"statements")
def migrate(cr, version):
cr.execute ("UPDATE account_bank_statement_line as sl "
" SET state = 'confirmed'"
" FROM account_bank_statement as s "
" WHERE sl.statement_id = s.id "
" AND s.state = 'confirm' "
)
cr.execute("UPDATE account_bank_statement_line as sl "
" SET state = 'confirmed'"
" FROM account_bank_statement as s "
" WHERE sl.statement_id = s.id "
" AND s.state = 'confirm' "
)

View File

@@ -19,6 +19,7 @@
#
##############################################################################
def migrate(cr, version):
if not version:
return
@@ -26,7 +27,7 @@ def migrate(cr, version):
# workflow state moved to another, new module
cr.execute(
"""
UPDATE ir_model_data
UPDATE ir_model_data
SET module = 'account_banking_payment'
WHERE name = 'trans_done_sent'
AND module = 'account_direct_debit'

View File

@@ -19,6 +19,7 @@
#
##############################################################################
def migrate(cr, version):
if not version:
return

View File

@@ -19,6 +19,7 @@
#
##############################################################################
def table_exists(cr, table):
""" Check whether a certain table or view exists """
cr.execute(
@@ -26,6 +27,7 @@ def table_exists(cr, table):
(table,))
return cr.fetchone()[0] == 1
def migrate(cr, version):
"""
Migration script for semantic changes in account_banking_payment_export.

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -19,6 +19,6 @@
#
##############################################################################
import models
from . import models
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -29,14 +29,17 @@ try:
except AttributeError:
from mx import DateTime as datetime
def str2date(datestr, format='%d/%m/%y'):
'''Convert a string to a datatime object'''
return datetime.strptime(datestr, format)
def date2str(date, format='%Y-%m-%d'):
'''Convert a datetime object to a string'''
return date.strftime(format)
def date2date(datestr, fromfmt='%d/%m/%y', tofmt='%Y-%m-%d'):
'''
Convert a date in a string to another string, in a different
@@ -44,7 +47,9 @@ def date2date(datestr, fromfmt='%d/%m/%y', tofmt='%Y-%m-%d'):
'''
return date2str(str2date(datestr, fromfmt), tofmt)
_SWIFT = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/-?:().,'+ "
_SWIFT = ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
"/-?:().,'+ ")
def to_swift(astr, schemes=['utf-8', 'latin-1', 'ascii']):
'''
@@ -62,7 +67,7 @@ def to_swift(astr, schemes=['utf-8', 'latin-1', 'ascii']):
s = [x in _SWIFT and x or ' '
for x in unicodedata.normalize('NFKD', astr).encode('ascii', 'ignore')
]
]
return ''.join(s)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -22,6 +22,7 @@
import re
from openerp.tools.translate import _
class mem_bank_statement(object):
'''
A mem_bank_statement is a real life projection of a bank statement paper
@@ -34,9 +35,15 @@ class mem_bank_statement(object):
'''
# Lock attributes to enable parsers to trigger non-conformity faults
__slots__ = [
'start_balance','end_balance', 'date', 'local_account',
'local_currency', 'id', 'transactions'
'start_balance',
'end_balance',
'date',
'local_account',
'local_currency',
'id',
'transactions'
]
def __init__(self, *args, **kwargs):
super(mem_bank_statement, self).__init__(*args, **kwargs)
self.id = ''
@@ -59,6 +66,7 @@ class mem_bank_statement(object):
check += float(transaction.transferred_amount)
return abs(check - float(self.end_balance)) < 0.0001
class mem_bank_transaction(object):
'''
A mem_bank_transaction is a real life copy of a bank transfer. Mapping to
@@ -102,7 +110,7 @@ class mem_bank_transaction(object):
# remote_currency
'transferred_amount',
# The actual amount transferred -
# The actual amount transferred -
# negative means sent, positive means received
# Most banks use the local_currency to express this amount, but there
# may be exceptions I'm unaware of.
@@ -126,7 +134,8 @@ class mem_bank_transaction(object):
# The other parties postal code belonging to the address
'remote_owner_country_code',
# The other parties two letter ISO country code belonging to the previous
# The other parties two letter ISO country code belonging to the
# previous
'remote_owner_custno',
# The other parties customer number
@@ -175,7 +184,7 @@ class mem_bank_transaction(object):
# An error message for interaction with the user
# Only used when mem_transaction.valid returns False.
'error_message',
# Storno attribute. When True, make the cancelled debit eligible for
# a next direct debit run
'storno_retry',
@@ -213,7 +222,7 @@ class mem_bank_transaction(object):
# Will be selected for matching.
# STORNO A failed or reversed attempt at direct debit.
# Either due to an action on the payer's side
# or a failure observed by the bank (lack of
# or a failure observed by the bank (lack of
# credit for instance)
#
# Perhaps more will follow.
@@ -230,7 +239,7 @@ class mem_bank_transaction(object):
DIRECT_DEBIT = 'DD'
ORDER = 'DO'
PAYMENT_BATCH = 'PB'
PAYMENT_TERMINAL = 'PT'
PAYMENT_TERMINAL = 'PT'
PERIODIC_ORDER = 'PO'
STORNO = 'ST'
@@ -270,7 +279,7 @@ class mem_bank_transaction(object):
if value in self.types:
self.transfer_type = value
else:
raise ValueError, _('Invalid value for transfer_type')
raise ValueError(_('Invalid value for transfer_type'))
type = property(_get_type, _set_type)
@@ -282,6 +291,7 @@ class mem_bank_transaction(object):
return (self.execution_date and self.remote_account
and self.transferred_amount and True) or False
class parser_type(type):
'''
Meta annex factory class for house keeping and collecting parsers.
@@ -314,11 +324,13 @@ class parser_type(type):
keys.sort()
return [(parsers[x].code, parsers[x].name) for x in keys]
def create_parser(code):
if code in parser_type.parser_by_code:
return parser_type.parser_by_code[code]()
return None
class parser(object):
'''
A parser delivers the interface for any parser object. Inherit from
@@ -384,7 +396,7 @@ class parser(object):
def parse(self, cr, data):
'''
Parse data.
data is a raw in memory file object. You have to split it in
whatever chunks you see fit for parsing. It should return a list
of mem_bank_statement objects. Every mem_bank_statement object
@@ -400,7 +412,7 @@ class parser(object):
be used as a prefix. Adding a tracer (day resolution) can create
uniqueness. Adding unique statement ids can add to the robustness of
your transaction numbering.
Just mind that users can create random (file)containers with
transactions in it. Try not to depend on order of appearance within
these files. If in doubt: sort.
@@ -408,5 +420,3 @@ class parser(object):
raise NotImplementedError(
_('This is a stub. Please implement your own.')
)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -31,16 +31,19 @@ from datetime import datetime, date
# Correct python2.4 issues
try:
datetime.strptime
def strpdate(str, format):
return datetime.strptime(str, format).date()
except AttributeError:
import time
def strpdate(str, format):
tm = time.strptime(str, format)
return date(tm.tm_year, tm.tm_mon, tm.tm_mday)
import unicodedata
class Field(object):
'''Base Field class - fixed length left aligned string field in a record'''
def __init__(self, name, length=1, fillchar=' ', cast=str):
@@ -57,11 +60,14 @@ class Field(object):
def take(self, buffer):
offset = hasattr(self, 'offset') and self.offset or 0
return self.cast(buffer[offset:offset + self.length].rstrip(self.fillchar))
return self.cast(buffer[offset:offset + self.length].rstrip(
self.fillchar)
)
def __repr__(self):
return '%s "%s"' % (self.__class__.__name__, self.name)
class Filler(Field):
'''Constant value field'''
def __init__(self, name, length=1, value=' '):
@@ -73,9 +79,10 @@ class Filler(Field):
def format(self, value):
return super(Filler, self).format(
self.value * (self.length / len(self.value) +1)
self.value * (self.length / len(self.value) + 1)
)
class DateField(Field):
'''Variable date field'''
def __init__(self, name, format='%Y-%m-%d', auto=False, cast=str):
@@ -98,6 +105,7 @@ class DateField(Field):
return strpdate(value, self.dateformat)
return self.auto and date.today() or None
class RightAlignedField(Field):
'''Deviation of Field: right aligned'''
def format(self, value):
@@ -107,7 +115,10 @@ class RightAlignedField(Field):
def take(self, buffer):
offset = hasattr(self, 'offset') and self.offset or 0
return self.cast(buffer[offset:offset + self.length].lstrip(self.fillchar))
return self.cast(buffer[offset:offset + self.length].lstrip(
self.fillchar)
)
class NumberField(RightAlignedField):
'''Deviation of Field: left zero filled'''
@@ -118,6 +129,7 @@ class NumberField(RightAlignedField):
def format(self, value):
return super(NumberField, self).format(self.cast(value or ''))
class RecordType(object):
fields = []
@@ -130,7 +142,7 @@ class RecordType(object):
offset += field.length
def __len__(self):
return reduce(lambda x,y: x+y.length, self.fields, 0)
return reduce(lambda x, y: x + y.length, self.fields, 0)
def __contains__(self, key):
return any(lambda x, y=key: x.name == y, self.fields)
@@ -139,7 +151,7 @@ class RecordType(object):
for field in self.fields:
if field.name == key:
return field
raise KeyError, 'No such field: %s' % key
raise KeyError('No such field: %s' % key)
def format(self, buffer):
result = []
@@ -150,7 +162,8 @@ class RecordType(object):
def take(self, buffer):
return dict(zip([x.name for x in self.fields],
[x.take(buffer) for x in self.fields]
))
))
class Record(object):
_recordtype = None
@@ -159,12 +172,12 @@ class Record(object):
if hasattr(self, '_fields') and self._fields:
self._recordtype = RecordType(self._fields)
if not self._recordtype and not recordtype:
raise ValueError, 'No recordtype specified'
raise ValueError('No recordtype specified')
if not self._recordtype:
self._recordtype = recordtype()
self._length = len(self._recordtype)
self._value = value.ljust(self._length)[:self._length]
def __len__(self):
return self._length
@@ -173,23 +186,24 @@ class Record(object):
super(Record, self).__setattr__(attr, value)
else:
field = self._recordtype[attr]
self._value = self._value[:field.offset] + \
field.format(value) + \
self._value[field.offset + field.length:]
self._value = (
self._value[:field.offset] +
field.format(value) +
self._value[field.offset + field.length:]
)
def __getattr__(self, attr):
if attr.startswith('_'):
return super(Record, self).__getattr__(attr)
field = self._recordtype[attr]
return field.take(self._value)
def __str__(self):
return self._recordtype.format(self._value)
def __unicode__(self):
return unicode(self.cast(self))
def asciify(str):
return unicodedata.normalize('NFKD', str).encode('ascii', 'ignore')
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright 2011 - 2014 Therp BV (<http://therp.nl>).
#
#
# 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
@@ -25,7 +25,7 @@ class ResBank(orm.Model):
def online_bank_info(self, cr, uid, bic, context=None):
"""
API hook for legacy online lookup of BICs,
API hook for legacy online lookup of BICs,
to be removed in OpenERP 8.0.
"""
return False, False

View File

@@ -30,7 +30,7 @@ class ResPartner(orm.Model):
self, cr, uid, ids, get_property_account, context=None):
"""
Returns the property journal account for the given partners ids.
:param get_property_account: method of this object that takes
a partner browse record and returns a field name of type many2one.
"""

View File

@@ -44,6 +44,7 @@
__all__ = ['IBAN', 'BBAN']
def modulo_97_base10(abuffer):
'''
Calculate the modulo 97 value of a string in base10
@@ -55,6 +56,7 @@ def modulo_97_base10(abuffer):
checksum %= 97
return checksum
def base36_to_base10str(abuffer):
'''
Convert a base36 string value to a string of base10 digits.
@@ -67,6 +69,7 @@ def base36_to_base10str(abuffer):
result += digit
return result
class BBANFormat(object):
'''
A BBANFormat is an auxilliary class for IBAN. It represents the composition
@@ -80,7 +83,7 @@ class BBANFormat(object):
Specify the structure of the SEPA account in relation to the local
account. The XXZZ prefix that all SEPA accounts have is not part of
the structure in BBANFormat.
ibanfmt: string of identifiers from position 5 (start = 1):
A = Account position
N = Account digit
@@ -95,7 +98,7 @@ class BBANFormat(object):
leading-zero-stripped account numbers.
Example: (NL) 'CCCCAAAAAAAAAA'
will convert 'INGB0001234567' into
will convert 'INGB0001234567' into
bankcode 'INGB' and account '0001234567'
bbanfmt: string of placeholders for the local bank account
@@ -119,7 +122,7 @@ class BBANFormat(object):
self._iban = ibanfmt
self._bban = bbanfmt
self._nolz = nolz
def __extract__(self, spec, value):
'''Extract the value based on the spec'''
i = self._iban.find(spec)
@@ -147,7 +150,7 @@ class BBANFormat(object):
else:
prefix = ''
return prefix + self.__extract__('A', iban)
def BBAN(self, iban):
'''
Format the BBAN part of the IBAN in iban following the local
@@ -178,6 +181,7 @@ class BBANFormat(object):
i += 1
return res
class IBAN(str):
'''
A IBAN string represents a SEPA bank account number. This class provides
@@ -272,7 +276,7 @@ class IBAN(str):
if item.isalnum():
init += item
elif item not in ' \t.-':
raise ValueError, 'Invalid chars found in IBAN number'
raise ValueError('Invalid chars found in IBAN number')
return str.__new__(cls, init)
def __init__(self, *args, **kwargs):
@@ -287,8 +291,7 @@ class IBAN(str):
@classmethod
def create(cls, BIC=None, countrycode=None, BBAN=None, bankcode=None,
branchcode=None, account=None
):
branchcode=None, account=None):
'''
Create a IBAN number from a BBAN and a country code. Optionaly create
a BBAN from BBAN components before generation.
@@ -304,20 +307,17 @@ class IBAN(str):
if countrycode:
countrycode = countrycode.upper()
else:
raise ValueError, \
'Either BIC or countrycode is required'
raise ValueError('Either BIC or countrycode is required')
if countrycode not in cls.countries:
raise ValueError, \
'%s is not a SEPA country' % countrycode
raise ValueError('%s is not a SEPA country' % countrycode)
format = cls.BBAN_formats[countrycode]
if BBAN:
if len(BBAN) == len(format._iban):
ibanno = cls(countrycode + '00' + BBAN)
return cls(countrycode + ibanno.checksum + BBAN)
raise ValueError, \
'Insufficient data to generate IBAN'
raise ValueError('Insufficient data to generate IBAN')
@property
def valid(self):
@@ -325,8 +325,10 @@ class IBAN(str):
Check if the string + check digits deliver a valid checksum
'''
_buffer = self[4:] + self[:4]
return self.countrycode in self.countries and \
int(base36_to_base10str(_buffer)) % 97 == 1
return (
self.countrycode in self.countries
and int(base36_to_base10str(_buffer)) % 97 == 1
)
def __repr__(self):
'''
@@ -387,7 +389,7 @@ class IBAN(str):
The bank code seems to be world wide unique. Knowing this,
one can use the country + bankcode info from BIC to narrow a
search for the bank itself.
Note that some countries use one single localization code for
all bank transactions in that country, while others do not. This
makes it impossible to use an algorithmic approach for generating
@@ -421,22 +423,24 @@ class IBAN(str):
'''
return self[4:]
class BBAN(object):
'''
Class to reformat a local BBAN account number to IBAN specs.
Simple validation based on length of spec string elements and real data.
'''
@staticmethod
def _get_length(fmt, element):
'''
Internal method to calculate the length of a parameter in a
formatted string
'''
i = 0; max_i = len(fmt._iban)
i = 0
max_i = len(fmt._iban)
while i < max_i:
if fmt._iban[i] == element:
next = i +1
next = i + 1
while next < max_i and fmt._iban[next] == element:
next += 1
return next - i
@@ -453,7 +457,10 @@ class BBAN(object):
if countrycode.upper() in IBAN.countries:
self._fmt = IBAN.BBAN_formats[countrycode.upper()]
res = ''
i = 0; j = 0; max_i = len(self._fmt._bban); max_j = len(bban)
i = 0
j = 0
max_i = len(self._fmt._bban)
max_j = len(bban)
while i < max_i and j < max_j:
while bban[j] in ' \t' and j < max_j:
j += 1
@@ -475,7 +482,7 @@ class BBAN(object):
# Note that many accounts in the IBAN standard
# are allowed to have leading zeros, so zfill
# to full spec length for visual validation.
#
#
# Note 2: this may look funny to some, as most
# local schemes strip leading zeros. It allows
# us however to present the user a visual feedback
@@ -512,15 +519,16 @@ class BBAN(object):
'''Simple check if BBAN is in the right format'''
return self._bban and True or False
if __name__ == '__main__':
import sys
for arg in sys.argv[1:]:
iban = IBAN(arg)
print 'IBAN:', iban
print 'country code:', iban.countrycode
print 'bank code:', iban.bankcode
print 'branch code:', iban.branchcode
print 'BBAN:', iban.BBAN
print 'localized BBAN:', iban.localized_BBAN
print 'check digits:', iban.checkdigits
print 'checksum:', iban.checksum
print('IBAN:', iban)
print('country code:', iban.countrycode)
print('bank code:', iban.bankcode)
print('branch code:', iban.branchcode)
print('BBAN:', iban.BBAN)
print('localized BBAN:', iban.localized_BBAN)
print('check digits:', iban.checkdigits)
print('checksum:', iban.checksum)

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -27,6 +27,7 @@ import re
__all__ = ['split', 'get', 'PostalCode']
class PostalCode(object):
'''
The PostalCode class is a wrapper around PostCodeFormat and an internal
@@ -46,11 +47,11 @@ class PostalCode(object):
'''
# Sort formats on length, longest first
formats = [(len(x), x) for x in format.split('|')]
formats = [x[1] for x in sorted(formats, lambda x,y: -cmp(x,y))]
self.res = [re.compile(x.replace('#', '\\d').replace('@','[A-Z]'))
formats = [x[1] for x in sorted(formats, lambda x, y: -cmp(x, y))]
self.res = [re.compile(x.replace('#', '\\d').replace('@', '[A-Z]'))
for x in formats
]
]
def get(self, str_):
'''
Return the postal code from the string str_
@@ -99,7 +100,8 @@ class PostalCode(object):
'IM': '@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',
'IL': '#####', 'IT': '####', 'JM': '', 'JP': '###-####',
'JE': '@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',
'JO': '#####', 'KZ': '######', 'KE': '#####', 'KI': '', 'KP': '###-###',
'JO': '#####', 'KZ': '######', 'KE': '#####', 'KI': '',
'KP': '###-###',
'KR': 'SEOUL ###-###', 'KW': '#####', 'KG': '######', 'LA': '#####',
'LV': 'LV-####', 'LB': '#### ####|####', 'LS': '###', 'LR': '####',
'LY': '', 'LI': '####', 'LT': 'LT-#####', 'LU': '####', 'MO': '',
@@ -139,7 +141,7 @@ class PostalCode(object):
country <iso>.
Returns iso, postal code and the remaining part of <str_>.
When iso is filled but postal code remains empty, no postal code could
be found according to the rules of iso.
@@ -155,14 +157,12 @@ class PostalCode(object):
# Find optimum (= max length postalcode) when iso code is unknown
all = {}
opt_iso = ''
max_l = 0
for key in cls._formats.iterkeys():
i, p, c = cls.split(str_, key)
l = len(p)
if l > max_l:
max_l = l
opt_iso = i
if l in all:
all[l].append((i, p, c))
else:

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
'''
Define a struct class which behaves like a dict, but allows using
object.attr alongside object['attr'].
@@ -25,6 +26,7 @@ object.attr alongside object['attr'].
__all__ = ['struct']
class struct(dict):
'''
Ease working with dicts. Allow dict.key alongside dict['key']
@@ -52,4 +54,4 @@ class struct(dict):
else:
fmt = '%*.*s%%s: %%s' % (indent, indent, '')
for item in self.iteritems():
print fmt % item
print(fmt % item)

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -18,8 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import bank_import
import banking_transaction_wizard
import link_partner
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import bank_import
from . import banking_transaction_wizard
from . import link_partner

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -23,11 +23,13 @@
# Kaspars Vilkens (KNdati): lenghty discussions, bugreports and bugfixes
# Stefan Rijnhart (Therp): bugreport and bugfix
#
'''
This module contains the business logic of the wizard account_banking_import.
The parsing is done in the parser modules. Every parser module is required to
use parser.models as a mean of communication with the business logic.
'''
import base64
import datetime
from openerp.osv import orm, fields
@@ -44,6 +46,7 @@ bt = models.mem_bank_transaction
# the real payment date. This can occur with online transactions (web shops).
payment_window = datetime.timedelta(days=10)
def parser_types(*args, **kwargs):
'''Delay evaluation of parser types until start of wizard, to allow
depending modules to initialize and add their parsers to the list
@@ -57,18 +60,26 @@ class banking_import_line(orm.TransientModel):
_columns = {
'name': fields.char('Name', size=64),
'date': fields.date('Date', readonly=True),
'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
'amount': fields.float(
'Amount',
digits_compute=dp.get_precision('Account'),
),
'statement_line_id': fields.many2one(
'account.bank.statement.line',
'Resulting statement line', readonly=True),
'type': fields.selection([
('supplier','Supplier'),
('customer','Customer'),
('general','General')
('supplier', 'Supplier'),
('customer', 'Customer'),
('general', 'General')
], 'Type', required=True),
'partner_id': fields.many2one('res.partner', 'Partner'),
'statement_id': fields.many2one('account.bank.statement', 'Statement',
select=True, required=True, ondelete='cascade'),
'statement_id': fields.many2one(
'account.bank.statement',
'Statement',
select=True,
required=True,
ondelete='cascade',
),
'ref': fields.char('Reference', size=32),
'note': fields.text('Notes'),
'period_id': fields.many2one('account.period', 'Period'),
@@ -83,16 +94,19 @@ class banking_import_line(orm.TransientModel):
'account.invoice', 'banking_import_line_invoice_rel',
'line_id', 'invoice_id'),
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account'),
'transaction_type': fields.selection([
'transaction_type': fields.selection(
[
# TODO: payment terminal etc...
('invoice', 'Invoice payment'),
('storno', 'Canceled debit order'),
('bank_costs', 'Bank costs'),
('unknown', 'Unknown'),
], 'Transaction type'),
],
'Transaction type',
),
'duplicate': fields.boolean('Duplicate'),
}
class banking_import(orm.TransientModel):
_name = 'account.banking.bank.import'
@@ -119,7 +133,8 @@ class banking_import(orm.TransientModel):
if not parser:
raise orm.except_orm(
_('ERROR!'),
_('Unable to import parser %(parser)s. Parser class not found.') %
_('Unable to import parser %(parser)s. Parser class not '
'found.') %
{'parser': parser_code}
)
@@ -133,16 +148,17 @@ class banking_import(orm.TransientModel):
if any([x for x in statements if not x.is_valid()]):
raise orm.except_orm(
_('ERROR!'),
_('The imported statements appear to be invalid! Check your file.')
_('The imported statements appear to be invalid! Check your '
'file.')
)
# Create the file now, as the statements need to be linked to it
import_id = statement_file_obj.create(cr, uid, dict(
company_id = company.id,
file = statements_file,
file_name = banking_import.file_name,
state = 'unfinished',
format = parser.name,
company_id=company.id,
file=statements_file,
file_name=banking_import.file_name,
state='unfinished',
format=parser.name,
))
bank_country_code = False
@@ -151,14 +167,14 @@ class banking_import(orm.TransientModel):
# Results
results = struct(
stat_loaded_cnt = 0,
trans_loaded_cnt = 0,
stat_skipped_cnt = 0,
trans_skipped_cnt = 0,
trans_matched_cnt = 0,
bank_costs_invoice_cnt = 0,
error_cnt = 0,
log = [],
stat_loaded_cnt=0,
trans_loaded_cnt=0,
stat_skipped_cnt=0,
trans_skipped_cnt=0,
trans_matched_cnt=0,
bank_costs_invoice_cnt=0,
error_cnt=0,
log=[],
)
# Caching
@@ -175,7 +191,9 @@ class banking_import(orm.TransientModel):
continue
# Create fallback currency code
currency_code = statement.local_currency or company.currency_id.name
currency_code = (
statement.local_currency or company.currency_id.name
)
# Check cache for account info/currency
if statement.local_account in info and \
@@ -190,8 +208,10 @@ class banking_import(orm.TransientModel):
)
if not account_info:
results.log.append(
_('Statements found for unknown account %(bank_account)s') %
{'bank_account': statement.local_account}
_('Statements found for unknown account '
'%(bank_account)s') % {
'bank_account': statement.local_account
}
)
error_accounts[statement.local_account] = True
results.error_cnt += 1
@@ -200,7 +220,7 @@ class banking_import(orm.TransientModel):
results.log.append(
_('Statements found for account %(bank_account)s, '
'but no default journal was defined.'
) % {'bank_account': statement.local_account}
) % {'bank_account': statement.local_account}
)
error_accounts[statement.local_account] = True
results.error_cnt += 1
@@ -210,7 +230,7 @@ class banking_import(orm.TransientModel):
currency_code = account_info.currency_id.name
# Cache results
if not statement.local_account in info:
if statement.local_account not in info:
info[statement.local_account] = {
currency_code: account_info
}
@@ -222,22 +242,22 @@ class banking_import(orm.TransientModel):
and account_info.currency_id.name != statement.local_currency:
# TODO: convert currencies?
results.log.append(
_('Statement %(statement_id)s for account %(bank_account)s'
_('Statement %(statement_id)s for account %(bank_account)s'
' uses different currency than the defined bank journal.'
) % {
'bank_account': statement.local_account,
'statement_id': statement.id
}
) % {
'bank_account': statement.local_account,
'statement_id': statement.id
}
)
error_accounts[statement.local_account] = True
results.error_cnt += 1
continue
# Check existence of previous statement
# Less well defined formats can resort to a
# Less well defined formats can resort to a
# dynamically generated statement identification
# (e.g. a datetime string of the moment of import)
# and have potential duplicates flagged by the
# and have potential duplicates flagged by the
# matching procedure
statement_ids = statement_obj.search(cr, uid, [
('name', '=', statement.id),
@@ -251,7 +271,8 @@ class banking_import(orm.TransientModel):
)
continue
# Get the period for the statement (as bank statement object checks this)
# Get the period for the statement (as bank statement object
# checks this)
period_ids = period_obj.search(
cr, uid, [
('company_id', '=', company.id),
@@ -259,7 +280,7 @@ class banking_import(orm.TransientModel):
('date_stop', '>=', statement.date),
('special', '=', False),
], context=context)
if not period_ids:
results.log.append(
_('No period found covering statement date %(date)s, '
@@ -272,17 +293,17 @@ class banking_import(orm.TransientModel):
# Create the bank statement record
statement_id = statement_obj.create(cr, uid, dict(
name = statement.id,
journal_id = account_info.journal_id.id,
date = convert.date2str(statement.date),
balance_start = statement.start_balance,
balance_end_real = statement.end_balance,
balance_end = statement.end_balance,
state = 'draft',
user_id = uid,
banking_id = import_id,
company_id = company.id,
period_id = period_ids[0],
name=statement.id,
journal_id=account_info.journal_id.id,
date=convert.date2str(statement.date),
balance_start=statement.start_balance,
balance_end_real=statement.end_balance,
balance_end=statement.end_balance,
state='draft',
user_id=uid,
banking_id=import_id,
company_id=company.id,
period_id=period_ids[0],
))
imported_statement_ids.append(statement_id)
@@ -294,7 +315,8 @@ class banking_import(orm.TransientModel):
values = {}
for attr in transaction.__slots__ + ['type']:
if attr in import_transaction_obj.column_map:
values[import_transaction_obj.column_map[attr]] = eval('transaction.%s' % attr)
values[import_transaction_obj.column_map[attr]] = \
eval('transaction.%s' % attr)
elif attr in import_transaction_obj._columns:
values[attr] = eval('transaction.%s' % attr)
values['statement_id'] = statement_id
@@ -305,35 +327,17 @@ class banking_import(orm.TransientModel):
transaction_id = import_transaction_obj.create(
cr, uid, values, context=context)
transaction_ids.append(transaction_id)
results.stat_loaded_cnt += 1
import_transaction_obj.match(cr, uid, transaction_ids, results=results, context=context)
#recompute statement end_balance for validation
import_transaction_obj.match(
cr, uid, transaction_ids, results=results, context=context
)
# recompute statement end_balance for validation
statement_obj.button_dummy(
cr, uid, imported_statement_ids, context=context)
# Original code. Didn't take workflow logistics into account...
#
#cr.execute(
# "UPDATE payment_order o "
# "SET state = 'done', "
# "date_done = '%s' "
# "FROM payment_line l "
# "WHERE o.state = 'sent' "
# "AND o.id = l.order_id "
# "AND l.id NOT IN ("
# "SELECT DISTINCT id FROM payment_line "
# "WHERE date_done IS NULL "
# "AND id IN (%s)"
# ")" % (
# time.strftime('%Y-%m-%d'),
# ','.join([str(x) for x in payment_line_ids])
# )
#)
report = [
'%s: %s' % (_('Total number of statements'),
results.stat_skipped_cnt + results.stat_loaded_cnt),
@@ -360,15 +364,18 @@ class banking_import(orm.TransientModel):
text_log = '\n'.join(report + results.log)
state = results.error_cnt and 'error' or 'ready'
statement_file_obj.write(cr, uid, import_id, dict(
state = state, log = text_log,
), context)
state=state,
log=text_log,
), context)
if not imported_statement_ids or not results.trans_loaded_cnt:
# file state can be 'ready' while import state is 'error'
state = 'error'
self.write(cr, uid, [ids[0]], dict(
import_id = import_id, log = text_log, state = state,
statement_ids = [(6, 0, imported_statement_ids)],
), context)
import_id=import_id,
log=text_log,
state=state,
statement_ids=[(6, 0, imported_statement_ids)],
), context)
return {
'name': (state == 'ready' and _('Review Bank Statements') or
_('Error')),
@@ -393,13 +400,15 @@ class banking_import(orm.TransientModel):
),
'file_name': fields.char('File name', size=256),
'file': fields.binary(
'Statements File', required=True,
help = ('The Transactions File to import. Please note that while it is '
'perfectly safe to reload the same file multiple times or to load in '
'timeframe overlapping statements files, there are formats that may '
'introduce different sequencing, which may create double entries.\n\n'
'To stay on the safe side, always load bank statements files using the '
'same format.'),
'Statements File',
required=True,
help=('The Transactions File to import. Please note that while it '
'is perfectly safe to reload the same file multiple times '
'or to load in timeframe overlapping statements files, '
'there are formats that may introduce different '
'sequencing, which may create double entries.\n\n'
'To stay on the safe side, always load bank statements '
'files using the same format.'),
states={
'ready': [('readonly', True)],
'error': [('readonly', True)],
@@ -435,8 +444,8 @@ class banking_import(orm.TransientModel):
_defaults = {
'state': 'init',
'company': lambda s,cr,uid,c:
'company': lambda s, cr, uid, c:
s.pool.get('res.company')._company_default_get(
cr, uid, 'bank.import.transaction', context=c),
cr, uid, 'bank.import.transaction', context=c),
'parser': _default_parser_type,
}

View File

@@ -7,8 +7,8 @@
# 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
# 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,
@@ -50,7 +50,7 @@ class banking_transaction_wizard(orm.TransientModel):
return res
def create_act_window(self, cr, uid, ids, nodestroy=True, context=None):
"""
"""
Return a popup window for this model
"""
if isinstance(ids, (int, long)):
@@ -78,10 +78,10 @@ class banking_transaction_wizard(orm.TransientModel):
import_transaction_obj = self.pool.get('banking.import.transaction')
trans_id = self.read(
cr, uid, ids[0], ['import_transaction_id'],
context=context)['import_transaction_id'][0] # many2one tuple
context=context)['import_transaction_id'][0] # many2one tuple
import_transaction_obj.match(cr, uid, [trans_id], context=context)
return self.create_act_window(cr, uid, ids, context=None)
def write(self, cr, uid, ids, vals, context=None):
"""
Implement a trigger to retrieve the corresponding move line
@@ -121,22 +121,27 @@ class banking_transaction_wizard(orm.TransientModel):
# Given the arity of the relation, there is are always
# multiple possibilities but the move lines here are
# prefiltered for having account_id.type payable/receivable
# and the regular invoice workflow should only come up with
# and the regular invoice workflow should only come up with
# one of those only.
for move_line in wiz.import_transaction_id.move_line_ids:
if (move_line.invoice ==
wiz.import_transaction_id.invoice_id):
transaction_obj.write(
cr, uid, wiz.import_transaction_id.id,
{ 'move_line_id': move_line.id, }, context=context)
{'move_line_id': move_line.id, },
context=context
)
statement_line_obj.write(
cr, uid, wiz.import_transaction_id.statement_line_id.id,
{ 'partner_id': move_line.partner_id.id or False,
'account_id': move_line.account_id.id,
}, context=context)
cr, uid,
wiz.import_transaction_id.statement_line_id.id,
{
'partner_id': (
move_line.partner_id.id or False),
'account_id': move_line.account_id.id,
}, context=context)
found = True
break
# Cannot match the invoice
# Cannot match the invoice
if not found:
orm.except_orm(
_("No entry found for the selected invoice"),
@@ -150,15 +155,16 @@ class banking_transaction_wizard(orm.TransientModel):
# Rewrite *2many directive notation
if manual_invoice_ids:
manual_invoice_ids = (
[i[1] for i in manual_invoice_ids if i[0]==4] +
[j for i in manual_invoice_ids if i[0]==6 for j in i[2]])
[i[1] for i in manual_invoice_ids if i[0] == 4] +
[j for i in manual_invoice_ids if i[0] == 6 for j in i[2]])
if manual_move_line_ids:
manual_move_line_ids = (
[i[1] for i in manual_move_line_ids if i[0]==4] +
[j for i in manual_move_line_ids if i[0]==6 for j in i[2]])
[i[1] for i in manual_move_line_ids if i[0] == 4] +
[j for i in manual_move_line_ids
if i[0] == 6 for j in i[2]])
for wiz in self.browse(cr, uid, ids, context=context):
#write can be called multiple times for the same values
#that doesn't hurt above, but it does here
# write can be called multiple times for the same values
# that doesn't hurt above, but it does here
if wiz.match_type and (
len(manual_move_line_ids) > 1 or
len(manual_invoice_ids) > 1):
@@ -171,7 +177,8 @@ class banking_transaction_wizard(orm.TransientModel):
found_move_line = False
if invoice.move_id:
for line in invoice.move_id.line_id:
if line.account_id.type in ('receivable', 'payable'):
if line.account_id.type in ('receivable',
'payable'):
todo.append((invoice.id, line.id))
found_move_line = True
break
@@ -181,12 +188,13 @@ class banking_transaction_wizard(orm.TransientModel):
_("No entry found for the selected invoice. "))
for move_line_id in manual_move_line_ids:
todo_entry = [False, move_line_id]
move_line=move_line_obj.read(
cr,
uid,
move_line_id,
['invoice'],
context=context)
move_line = move_line_obj.read(
cr,
uid,
move_line_id,
['invoice'],
context=context
)
if move_line['invoice']:
todo_entry[0] = move_line['invoice'][0]
todo.append(todo_entry)
@@ -194,25 +202,27 @@ class banking_transaction_wizard(orm.TransientModel):
while todo:
todo_entry = todo.pop()
move_line = move_line_obj.browse(
cr, uid, todo_entry[1], context)
cr, uid, todo_entry[1], context)
transaction_id = wiz.import_transaction_id.id
statement_line_id = wiz.statement_line_id.id
if len(todo) > 0:
statement_line_id = wiz.statement_line_id.split_off(
move_line.debit or -move_line.credit)[0]
move_line.debit or -move_line.credit)[0]
transaction_id = statement_line_obj.browse(
cr,
uid,
statement_line_id,
context=context).import_transaction_id.id
cr,
uid,
statement_line_id,
context=context
).import_transaction_id.id
vals = {
'move_line_id': todo_entry[1],
'move_line_ids': [(6, 0, [todo_entry[1]])],
'invoice_id': todo_entry[0],
'invoice_ids': [(6, 0,
[todo_entry[0]] if todo_entry[0] else [])],
'invoice_ids': [
(6, 0, [todo_entry[0]] if todo_entry[0] else [])
],
'match_type': 'manual',
}
@@ -221,7 +231,7 @@ class banking_transaction_wizard(orm.TransientModel):
st_line_vals = {
'account_id': move_line_obj.read(
cr, uid, todo_entry[1],
cr, uid, todo_entry[1],
['account_id'], context=context)['account_id'][0],
}
@@ -231,7 +241,7 @@ class banking_transaction_wizard(orm.TransientModel):
).partner_id.commercial_partner_id.id
statement_line_obj.write(
cr, uid, statement_line_id,
cr, uid, statement_line_id,
st_line_vals, context=context)
return res
@@ -255,22 +265,19 @@ class banking_transaction_wizard(orm.TransientModel):
# Get the bank account setting record, to reset the account
account_id = False
journal_id = wiz.statement_line_id.statement_id.journal_id.id
setting_ids = settings_pool.find(cr, uid, journal_id, context=context)
setting_ids = settings_pool.find(
cr, uid, journal_id, context=context
)
# Restore partner id from the bank account or else reset
partner_id = False
if (wiz.statement_line_id.partner_bank_id and
wiz.statement_line_id.partner_bank_id.partner_id):
partner_id = wiz.statement_line_id.partner_bank_id.partner_id.id
partner_id = (
wiz.statement_line_id.partner_bank_id.partner_id.id
)
wiz.write({'partner_id': partner_id})
# Select account type by parter customer or supplier,
# or default based on amount sign
if wiz.amount < 0:
account_type = 'payable'
else:
account_type = 'receivable'
bank_partner = False
if partner_id:
bank_partner = wiz.statement_line_id.partner_bank_id.partner_id
@@ -295,13 +302,18 @@ class banking_transaction_wizard(orm.TransientModel):
wiz.statement_line_id.write({'account_id': account_id})
if wiz.statement_line_id:
#delete splits causing an unsplit if this is a split
#transaction
statement_pool.unlink(cr, uid,
statement_pool.search(cr, uid,
[('parent_id', '=', wiz.statement_line_id.id)],
context=context),
context=context)
# delete splits causing an unsplit if this is a split
# transaction
statement_pool.unlink(
cr,
uid,
statement_pool.search(
cr, uid,
[('parent_id', '=', wiz.statement_line_id.id)],
context=context
),
context=context
)
if wiz.import_transaction_id:
wiz.import_transaction_id.clear_and_write()
@@ -313,15 +325,15 @@ class banking_transaction_wizard(orm.TransientModel):
ids = [ids]
transaction_obj = self.pool.get('banking.import.transaction')
for wiz in self.read(
cr, uid, ids, ['duplicate', 'import_transaction_id'],
context=context):
cr, uid, ids, ['duplicate', 'import_transaction_id'],
context=context):
transaction_obj.write(
cr, uid, wiz['import_transaction_id'][0],
cr, uid, wiz['import_transaction_id'][0],
{'duplicate': not wiz['duplicate']}, context=context)
return self.create_act_window(cr, uid, ids, context=None)
def button_done(self, cr, uid, ids, context=None):
return {'type': 'ir.actions.act_window_close'}
return {'type': 'ir.actions.act_window_close'}
_columns = {
'name': fields.char('Name', size=64),
@@ -349,22 +361,26 @@ class banking_transaction_wizard(orm.TransientModel):
'statement_line_id', 'parent_id', type='many2one',
relation='account.bank.statement.line', readonly=True),
'import_transaction_id': fields.related(
'statement_line_id', 'import_transaction_id',
'statement_line_id', 'import_transaction_id',
string="Import transaction",
type='many2one', relation='banking.import.transaction'),
'residual': fields.related(
'import_transaction_id', 'residual', type='float',
'import_transaction_id', 'residual', type='float',
string='Residual', readonly=True),
'writeoff_account_id': fields.related(
'import_transaction_id', 'writeoff_account_id',
type='many2one', relation='account.account',
string='Write-off account'),
'invoice_ids': fields.related(
'import_transaction_id', 'invoice_ids', string="Matching invoices",
'import_transaction_id', 'invoice_ids', string="Matching invoices",
type='many2many', relation='account.invoice'),
'invoice_id': fields.related(
'import_transaction_id', 'invoice_id', string="Invoice to reconcile",
type='many2one', relation='account.invoice'),
'import_transaction_id',
'invoice_id',
string="Invoice to reconcile",
type='many2one',
relation='account.invoice',
),
'move_line_ids': fields.related(
'import_transaction_id', 'move_line_ids', string="Entry lines",
type='many2many', relation='account.move.line'),
@@ -372,15 +388,20 @@ class banking_transaction_wizard(orm.TransientModel):
'import_transaction_id', 'move_line_id', string="Entry line",
type='many2one', relation='account.move.line'),
'duplicate': fields.related(
'import_transaction_id', 'duplicate', string='Flagged as duplicate',
type='boolean'),
'import_transaction_id',
'duplicate',
string='Flagged as duplicate',
type='boolean',
),
'match_multi': fields.related(
'import_transaction_id', 'match_multi',
'import_transaction_id', 'match_multi',
type="boolean", string='Multiple matches'),
'match_type': fields.related(
'import_transaction_id', 'match_type', type='selection',
'import_transaction_id',
'match_type',
type='selection',
selection=[
('move','Move'),
('move', 'Move'),
('invoice', 'Invoice'),
('payment', 'Payment line'),
('payment_order', 'Payment order'),
@@ -388,8 +409,10 @@ class banking_transaction_wizard(orm.TransientModel):
('manual', 'Manual'),
('payment_manual', 'Payment line (manual)'),
('payment_order_manual', 'Payment order (manual)'),
],
string='Match type', readonly=True),
],
string='Match type',
readonly=True,
),
'manual_invoice_ids': fields.many2many(
'account.invoice',
'banking_transaction_wizard_account_invoice_rel',
@@ -401,8 +424,17 @@ class banking_transaction_wizard(orm.TransientModel):
'wizard_id', 'move_line_id', string='Or match one or more entries',
domain=[('account_id.reconcile', '=', True),
('reconcile_id', '=', False)]),
'payment_option': fields.related('import_transaction_id','payment_option', string='Payment Difference', type='selection', required=True,
selection=[('without_writeoff', 'Keep Open'),('with_writeoff', 'Reconcile Payment Balance')]),
'payment_option': fields.related(
'import_transaction_id',
'payment_option',
string='Payment Difference',
type='selection',
required=True,
selection=[
('without_writeoff', 'Keep Open'),
('with_writeoff', 'Reconcile Payment Balance')
],
),
'writeoff_analytic_id': fields.related(
'import_transaction_id', 'writeoff_analytic_id',
type='many2one', relation='account.analytic.account',
@@ -411,9 +443,11 @@ class banking_transaction_wizard(orm.TransientModel):
'statement_line_id', 'analytic_account_id',
type='many2one', relation='account.analytic.account',
string="Analytic Account"),
'move_currency_amount': fields.related('import_transaction_id','move_currency_amount',
type='float', string='Match Currency Amount', readonly=True),
}
banking_transaction_wizard()
'move_currency_amount': fields.related(
'import_transaction_id',
'move_currency_amount',
type='float',
string='Match Currency Amount',
readonly=True,
),
}

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -24,7 +24,7 @@ from openerp.addons.account_banking import sepa
from openerp.addons.account_banking.struct import struct
__all__ = [
'get_period',
'get_period',
'get_bank_accounts',
'get_partner',
'get_country_id',
@@ -32,6 +32,7 @@ __all__ = [
'create_bank_account',
]
def get_period(pool, cr, uid, date, company, log=None):
'''
Wrapper over account_period.find() to log exceptions of
@@ -43,7 +44,7 @@ def get_period(pool, cr, uid, date, company, log=None):
try:
period_ids = pool.get('account.period').find(
cr, uid, dt=date, context=context)
except Exception, e:
except Exception as e:
if log is None:
raise
else:
@@ -51,6 +52,7 @@ def get_period(pool, cr, uid, date, company, log=None):
return False
return period_ids[0]
def get_bank_accounts(pool, cr, uid, account_number, log, fail=False):
'''
Get the bank account with account number account_number
@@ -72,6 +74,7 @@ def get_bank_accounts(pool, cr, uid, account_number, log, fail=False):
return []
return partner_bank_obj.browse(cr, uid, bank_account_ids)
def _has_attr(obj, attr):
# Needed for dangling addresses and a weird exception scheme in
# OpenERP's orm.
@@ -80,6 +83,7 @@ def _has_attr(obj, attr):
except KeyError:
return False
def get_partner(pool, cr, uid, name, address, postal_code, city,
country_id, log, context=None):
'''
@@ -87,7 +91,7 @@ def get_partner(pool, cr, uid, name, address, postal_code, city,
If multiple partners are found with the same name, select the first and
add a warning to the import log.
TODO: revive the search by lines from the address argument
'''
partner_obj = pool.get('res.partner')
@@ -115,7 +119,8 @@ def get_partner(pool, cr, uid, name, address, postal_code, city,
key = name.lower()
partners = []
for partner in partner_obj.read(
cr, uid, partner_search_ids, ['name', 'commercial_partner_id'], context=context):
cr, uid, partner_search_ids, ['name', 'commercial_partner_id'],
context=context):
if (len(partner['name']) > 3 and partner['name'].lower() in key):
partners.append(partner)
partners.sort(key=lambda x: len(x['name']), reverse=True)
@@ -126,6 +131,7 @@ def get_partner(pool, cr, uid, name, address, postal_code, city,
'name %(name)s') % {'name': name})
return partner_ids and partner_ids[0] or False
def get_company_bank_account(pool, cr, uid, account_number, currency,
company, log):
'''
@@ -139,16 +145,16 @@ def get_company_bank_account(pool, cr, uid, account_number, currency,
return False
elif len(bank_accounts) != 1:
log.append(
_('More than one bank account was found with the same number %(account_no)s')
% dict(account_no = account_number)
_('More than one bank account was found with the same number '
'%(account_no)s') % dict(account_no=account_number)
)
return False
if bank_accounts[0].partner_id.id != company.partner_id.id:
log.append(
_('Account %(account_no)s is not owned by %(partner)s')
% dict(account_no = account_number,
partner = company.partner_id.name,
))
% dict(account_no=account_number,
partner=company.partner_id.name,
))
return False
results.account = bank_accounts[0]
bank_settings_obj = pool.get('account.banking.account.settings')
@@ -189,8 +195,9 @@ def get_company_bank_account(pool, cr, uid, account_number, currency,
return results
def get_or_create_bank(pool, cr, uid, bic, online=False, code=None,
name=None):
name=None, context=None):
'''
Find or create the bank with the provided BIC code.
When online, the SWIFT database will be consulted in order to
@@ -231,38 +238,41 @@ def get_or_create_bank(pool, cr, uid, bic, online=False, code=None,
bank_id = False
if online:
info, address = bank_obj.online_bank_info(cr, uid, bic, context=context)
info, address = bank_obj.online_bank_info(
cr, uid, bic, context=context
)
if info:
bank_id = bank_obj.create(cr, uid, dict(
code = info.code,
name = info.name,
street = address.street,
street2 = address.street2,
zip = address.zip,
city = address.city,
country = country_id,
bic = info.bic[:8],
code=info.code,
name=info.name,
street=address.street,
street2=address.street2,
zip=address.zip,
city=address.city,
country=country_id,
bic=info.bic[:8],
))
else:
info = struct(name=name, code=code)
if not online or not bank_id:
bank_id = bank_obj.create(cr, uid, dict(
code = info.code or 'UNKNOW',
name = info.name or _('Unknown Bank'),
country = country_id,
bic = bic,
code=info.code or 'UNKNOW', # FIXME: Typo?
name=info.name or _('Unknown Bank'),
country=country_id,
bic=bic,
))
return bank_id, country_id
def get_country_id(pool, cr, uid, transaction, context=None):
"""
Derive a country id from the info on the transaction.
:param transaction: browse record of a transaction
:returns: res.country id or False
:returns: res.country id or False
"""
country_code = False
iban = sepa.IBAN(transaction.remote_account)
if iban.valid:
@@ -283,6 +293,7 @@ def get_country_id(pool, cr, uid, transaction, context=None):
country_id = company.partner_id.country.id
return country_id
def create_bank_account(pool, cr, uid, partner_id,
account_number, holder_name, address, city,
country_id, bic=False,
@@ -291,9 +302,9 @@ def create_bank_account(pool, cr, uid, partner_id,
Create a matching bank account with this holder for this partner.
'''
values = struct(
partner_id = partner_id,
owner_name = holder_name,
country_id = country_id,
partner_id=partner_id,
owner_name=holder_name,
country_id=country_id,
)
# Are we dealing with IBAN?
@@ -325,5 +336,3 @@ def create_bank_account(pool, cr, uid, partner_id,
# Create bank account and return
return pool.get('res.partner.bank').create(
cr, uid, values, context=context)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -24,6 +24,7 @@ from openerp.tools.translate import _
from openerp.addons.account_banking.wizard import banktools
import ast
class link_partner(orm.TransientModel):
_name = 'banking.link_partner'
_description = 'Link partner'
@@ -66,14 +67,14 @@ class link_partner(orm.TransientModel):
'mobile': fields.char('Mobile', size=64),
'is_company': fields.boolean('Is a Company'),
}
_defaults = {
'is_company': True,
}
def create(self, cr, uid, vals, context=None):
"""
Get default values from the transaction data
Get default values from the transaction data
on the statement line
"""
if vals and vals.get('statement_line_id'):
@@ -86,7 +87,7 @@ class link_partner(orm.TransientModel):
raise orm.except_orm(
_('Error'),
_('Statement line is already linked to a bank account '))
if not(transaction
and transaction.remote_account):
raise orm.except_orm(
@@ -124,17 +125,17 @@ class link_partner(orm.TransientModel):
return super(link_partner, self).create(
cr, uid, vals, context=context)
def update_partner_values(self, cr, uid, wizard, values, context=None):
"""
Updates the new partner values with the values from the wizard
:param wizard: read record of wizard (with load='_classic_write')
:param values: the dictionary of partner values that will be updated
"""
for field in ['is_company',
'name',
'street',
'street',
'street2',
'zip',
'city',
@@ -148,7 +149,7 @@ class link_partner(orm.TransientModel):
if wizard[field]:
values[field] = wizard[field]
return True
def link_partner(self, cr, uid, ids, context=None):
statement_line_obj = self.pool.get(
'account.bank.statement.line')
@@ -160,13 +161,13 @@ class link_partner(orm.TransientModel):
wiz_read = self.read(
cr, uid, ids[0], context=context, load='_classic_write')
partner_vals = {
'type': 'default',
}
'type': 'default',
}
self.update_partner_values(
cr, uid, wiz_read, partner_vals, context=context)
partner_id = self.pool.get('res.partner').create(
cr, uid, partner_vals, context=context)
partner_bank_id = banktools.create_bank_account(
self.pool, cr, uid, partner_id,
wiz.remote_account, wiz.name,
@@ -185,10 +186,10 @@ class link_partner(orm.TransientModel):
{'partner_bank_id': partner_bank_id,
'partner_id': partner_id}, context=context)
return {'type': 'ir.actions.act_window_close'}
return {'type': 'ir.actions.act_window_close'}
def create_act_window(self, cr, uid, ids, nodestroy=True, context=None):
"""
"""
Return a popup window for this model
"""
if isinstance(ids, (int, long)):
@@ -205,5 +206,3 @@ class link_partner(orm.TransientModel):
'res_id': ids[0],
'nodestroy': nodestroy,
}

View File

@@ -4,8 +4,8 @@
# 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
# 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,

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -26,6 +26,7 @@ from openerp.addons.account_banking.parsers.convert import str2date
bt = models.mem_bank_transaction
class transaction(models.mem_bank_transaction):
def __init__(self, values, *args, **kwargs):
@@ -36,6 +37,7 @@ class transaction(models.mem_bank_transaction):
def is_valid(self):
return not self.error_message
class parser(models.parser):
code = 'CAMT'
country_code = 'NL'
@@ -69,7 +71,7 @@ CAMT Format parser
def find(self, node, expr):
"""
Like xpath(), but return first result if any or else False
Return None to test nodes for being truesy
"""
result = node.xpath(expr, namespaces={'ns': self.ns[1:-1]})
@@ -82,19 +84,20 @@ CAMT Format parser
:param node: BkToCstmrStmt/Stmt/Bal node
:param balance type: one of 'OPBD', 'PRCD', 'ITBD', 'CLBD'
"""
code_expr = './ns:Bal/ns:Tp/ns:CdOrPrtry/ns:Cd[text()="%s"]/../../..' % balance_type
code_expr = ('./ns:Bal/ns:Tp/ns:CdOrPrtry/ns:Cd[text()="%s"]/../../..'
% balance_type)
return self.xpath(node, code_expr)
def parse_amount(self, node):
"""
Parse an element that contains both Amount and CreditDebitIndicator
:return: signed amount
:returntype: float
"""
sign = -1 if node.find(self.ns + 'CdtDbtInd').text == 'DBIT' else 1
return sign * float(node.find(self.ns + 'Amt').text)
def get_start_balance(self, node):
"""
Find the (only) balance node with code OpeningBalance, or
@@ -240,7 +243,7 @@ CAMT Format parser
structured = self.find(
TxDtls, './ns:RmtInf/ns:Strd/ns:CdtrRefInf/ns:Ref')
if structured is None or not structured.text:
structured = self.find(TxDtls, './ns:Refs/ns:EndToEndId')
structured = self.find(TxDtls, './ns:Refs/ns:EndToEndId')
if structured is not None:
vals['reference'] = structured.text
else:

View File

@@ -12,8 +12,8 @@
# garantees and support are strongly adviced to contract EduSense BV
#
# 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
# 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,
@@ -25,6 +25,5 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import patu
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import patu

View File

@@ -11,8 +11,8 @@
# garantees and support are strongly adviced to contract EduSense BV
#
# 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
# 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,
@@ -24,6 +24,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Account Banking PATU module',
'version': '0.62',

View File

@@ -1,7 +1,9 @@
#!/usr/bin/env python
# encoding: utf-8
"""Parser for PATU format files"""
import re, datetime
import re
import datetime
def fixchars(line):
"""Fix the characters mangled in the input
@@ -20,107 +22,120 @@ def fixchars(line):
class PatuParser(object):
"""Parse PATU lines in to structs"""
def __init__( self ):
def __init__(self):
""" Initialize PATU parser """
recparse = dict()
recparse["00"] = "T(?P<recordid>00)(?P<record_len>\d{3})" \
+ "(?P<version>\d{3})(?P<accountnr>\d{14})" \
+ "(?P<statementnr>\d{3})(?P<startdate>\d{6})" \
+ "(?P<enddate>\d{6})" \
+ "(?P<creationdate>\d{6})(?P<creationtime>\d{4})" \
+ "(?P<customerid>.{17})(?P<balancedate>\d{6})" \
+ "(?P<startingbalance>.{19})" \
+ "(?P<itemcount>\d{6})(?P<currency>.{3})" \
+ "(?P<accountname>.{30})"\
+ "(?P<accountlimit>\d{18})(?P<accountowner>.{35})" \
+ "(?P<bankcontact1>.{40})(?P<bankcontact2>.{40})" \
+ "(?P<bankcontact3>.{30})(?P<ibanswift>.{30})"
recparse["10"] = "T(?P<recordid>[18]0)(?P<record_len>\d{3})" \
+ "(?P<eventid>\d{6})" \
+ "(?P<archivalnr>.{18})(?P<recorddate>\d{6})" \
+ "(?P<valuedate>\d{6})" \
+ "(?P<paymentdate>\d{6})(?P<eventtype>\d)" \
+ "(?P<eventcode>.{3})(?P<eventdesc>.{35})" \
+ "(?P<amount>.{19})(?P<receiptcode>.)(?P<creationmethod>.)" \
+ "(?P<recipientname>.{35})(?P<recipientsource>.)" \
+ "(?P<recipientaccount>.{14})(?P<recipientaccountchanged>.)" \
+ "(?P<refnr>.{20})" \
+ "(?P<formnr>.{8})(?P<eventlevel>.)"
recparse["11"] = "T(?P<recordid>[18]1)(?P<record_len>\d{3})" \
+ "(?P<infotype>.{2})" \
+ "(?:(?# Match specific info)" \
+ "(?<=00)(?P<message>.{35})+" \
+ "|" \
+ "(?<=01)(?P<transactioncount>\d{8})" \
+ "|" \
+ "(?<=02)(?P<customerid>.{10})\s(?P<invoicenr>.{15})\s" \
+ "(?P<invoicedate>\d{6})" \
+ "|" \
+ "(?<=03)(?P<cardnumber>.{19})\s(?P<storereference>.{14})" \
+ "|" \
+ "(?<=04)(?P<origarchiveid>.{18})" \
+ "|" \
+ "(?<=05)(?P<destinationamount>.{19})\s(?P<currency>.{3})\s" \
+ "(?P<exchangerate>.{11})(?P<rateref>.{6})" \
+ "|" \
+ "(?<=06)(?P<principalinfo1>.{35})(?P<principalinfo2>.{35})" \
+ "|" \
+ "(?<=07)(?P<bankinfo1>.{35})" \
+ "(?P<bankinfo2>.{35})?" \
+ "(?P<bankinfo3>.{35})?" \
+ "(?P<bankinfo4>.{35})?" \
+ "(?P<bankinfo5>.{35})?" \
+ "(?P<bankinfo6>.{35})?" \
+ "(?P<bankinfo7>.{35})?" \
+ "(?P<bankinfo8>.{35})?" \
+ "(?P<bankinfo9>.{35})?" \
+ "(?P<bankinfo10>.{35})?" \
+ "(?P<bankinfo11>.{35})?" \
+ "(?P<bankinfo12>.{35})?" \
+ "|" \
+ "(?<=08)(?P<paymentcode>\d{3})\s(?P<paymentdesc>.{31})" \
+ "|" \
+ "(?<=09)(?P<recipientname2>.{35})" \
+ "|" \
+ "(?<=11)(?P<reference>.{35})(?P<recipientiban>.{35})" \
+ "(?P<recipientbic>.{35})(?P<recipientnameiban>.{70})" \
+ "(?P<sendername>.{70})(?P<senderid>.{35})" \
+ "(?P<archivalid>.{70})" \
+ ")"
recparse["40"] = "T(?P<recordid>40)(?P<record_len>\d{3})" \
+ "(?P<recorddate>\d{6})(?P<balance>.{19})" \
+ "(?P<availablefunds>.{19})"
recparse["50"] = "T(?P<recordid>50)(?P<record_len>\d{3})" \
+ "(?P<period>\d)(?P<perioddate>\d{6})" \
+ "(?P<depositcount>\d{8})(?P<depositsum>.{19})" \
+ "(?P<withdrawcount>\d{8})(?P<withdrawsum>.{19})"
recparse["60"] = "T(?P<recordid>60)(?P<record_len>\d{3})" \
+ "(?P<bankid>.{3})(?P<specialid>01)" \
+ "(?P<interestperiodstart>\d{6})-" \
+ "(?P<interestperiodend>\d{6})" \
+ "(?P<avgbalanceinfo>.)(?P<avgbalance>.{19})" \
+ "(?P<interestinfo>.)(?P<interestrate>\d{7})" \
+ "(?P<limitbalanceinfo>.)(?P<avglimitbalance>.{19})" \
+ "(?P<limitinterestinfo>.)(?P<limitinterestrate>\d{7})" \
+ "(?P<limitusageinfo>.)(?P<limitusage>\d{7})" \
+ "(?P<permanentbalanceinfo>.)(?P<permanentbalance>.{19})" \
+ "(?P<refinterestinfo>.)(?P<refinterestname>.{35})" \
+ "(?P<refinterestrate>\d{7})" \
+ "(?P<refcreditinfo>.)(?P<refcreditname>.{35})" \
+ "(?P<refcreditrate>\d{7})"
recparse["70"] = "T(?P<recordid>70)(?P<record_len>\d{3})" \
+ "(?P<bankid>\d{3})" \
+ "(?P<infoline1>.{80})" \
+ "(?P<infoline2>.{80})?" \
+ "(?P<infoline3>.{80})?" \
+ "(?P<infoline4>.{80})?" \
+ "(?P<infoline5>.{80})?" \
+ "(?P<infoline6>.{80})?"
recparse["00"] = (
"T(?P<recordid>00)(?P<record_len>\d{3})"
"(?P<version>\d{3})(?P<accountnr>\d{14})"
"(?P<statementnr>\d{3})(?P<startdate>\d{6})"
"(?P<enddate>\d{6})"
"(?P<creationdate>\d{6})(?P<creationtime>\d{4})"
"(?P<customerid>.{17})(?P<balancedate>\d{6})"
"(?P<startingbalance>.{19})"
"(?P<itemcount>\d{6})(?P<currency>.{3})"
"(?P<accountname>.{30})"
"(?P<accountlimit>\d{18})(?P<accountowner>.{35})"
"(?P<bankcontact1>.{40})(?P<bankcontact2>.{40})"
"(?P<bankcontact3>.{30})(?P<ibanswift>.{30})"
)
recparse["10"] = (
"T(?P<recordid>[18]0)(?P<record_len>\d{3})"
"(?P<eventid>\d{6})"
"(?P<archivalnr>.{18})(?P<recorddate>\d{6})"
"(?P<valuedate>\d{6})"
"(?P<paymentdate>\d{6})(?P<eventtype>\d)"
"(?P<eventcode>.{3})(?P<eventdesc>.{35})"
"(?P<amount>.{19})(?P<receiptcode>.)(?P<creationmethod>.)"
"(?P<recipientname>.{35})(?P<recipientsource>.)"
"(?P<recipientaccount>.{14})(?P<recipientaccountchanged>.)"
"(?P<refnr>.{20})"
"(?P<formnr>.{8})(?P<eventlevel>.)"
)
recparse["11"] = (
"T(?P<recordid>[18]1)(?P<record_len>\d{3})"
"(?P<infotype>.{2})"
"(?:(?# Match specific info)"
"(?<=00)(?P<message>.{35})+"
"|"
"(?<=01)(?P<transactioncount>\d{8})"
"|"
"(?<=02)(?P<customerid>.{10})\s(?P<invoicenr>.{15})\s"
"(?P<invoicedate>\d{6})"
"|"
"(?<=03)(?P<cardnumber>.{19})\s(?P<storereference>.{14})"
"|"
"(?<=04)(?P<origarchiveid>.{18})"
"|"
"(?<=05)(?P<destinationamount>.{19})\s(?P<currency>.{3})\s"
"(?P<exchangerate>.{11})(?P<rateref>.{6})"
"|"
"(?<=06)(?P<principalinfo1>.{35})(?P<principalinfo2>.{35})"
"|"
"(?<=07)(?P<bankinfo1>.{35})"
"(?P<bankinfo2>.{35})?"
"(?P<bankinfo3>.{35})?"
"(?P<bankinfo4>.{35})?"
"(?P<bankinfo5>.{35})?"
"(?P<bankinfo6>.{35})?"
"(?P<bankinfo7>.{35})?"
"(?P<bankinfo8>.{35})?"
"(?P<bankinfo9>.{35})?"
"(?P<bankinfo10>.{35})?"
"(?P<bankinfo11>.{35})?"
"(?P<bankinfo12>.{35})?"
"|"
"(?<=08)(?P<paymentcode>\d{3})\s(?P<paymentdesc>.{31})"
"|"
"(?<=09)(?P<recipientname2>.{35})"
"|"
"(?<=11)(?P<reference>.{35})(?P<recipientiban>.{35})"
"(?P<recipientbic>.{35})(?P<recipientnameiban>.{70})"
"(?P<sendername>.{70})(?P<senderid>.{35})"
"(?P<archivalid>.{70})"
")"
)
recparse["40"] = (
"T(?P<recordid>40)(?P<record_len>\d{3})"
"(?P<recorddate>\d{6})(?P<balance>.{19})"
"(?P<availablefunds>.{19})"
)
recparse["50"] = (
"T(?P<recordid>50)(?P<record_len>\d{3})"
"(?P<period>\d)(?P<perioddate>\d{6})"
"(?P<depositcount>\d{8})(?P<depositsum>.{19})"
"(?P<withdrawcount>\d{8})(?P<withdrawsum>.{19})"
)
recparse["60"] = (
"T(?P<recordid>60)(?P<record_len>\d{3})"
"(?P<bankid>.{3})(?P<specialid>01)"
"(?P<interestperiodstart>\d{6})-"
"(?P<interestperiodend>\d{6})"
"(?P<avgbalanceinfo>.)(?P<avgbalance>.{19})"
"(?P<interestinfo>.)(?P<interestrate>\d{7})"
"(?P<limitbalanceinfo>.)(?P<avglimitbalance>.{19})"
"(?P<limitinterestinfo>.)(?P<limitinterestrate>\d{7})"
"(?P<limitusageinfo>.)(?P<limitusage>\d{7})"
"(?P<permanentbalanceinfo>.)(?P<permanentbalance>.{19})"
"(?P<refinterestinfo>.)(?P<refinterestname>.{35})"
"(?P<refinterestrate>\d{7})"
"(?P<refcreditinfo>.)(?P<refcreditname>.{35})"
"(?P<refcreditrate>\d{7})"
)
recparse["70"] = (
"T(?P<recordid>70)(?P<record_len>\d{3})"
"(?P<bankid>\d{3})"
"(?P<infoline1>.{80})"
"(?P<infoline2>.{80})?"
"(?P<infoline3>.{80})?"
"(?P<infoline4>.{80})?"
"(?P<infoline5>.{80})?"
"(?P<infoline6>.{80})?"
)
for record in recparse:
recparse[record] = re.compile(recparse[record])
self.recparse = recparse
def parse_record(self, line):
"""Docstring for parse_perus
@@ -135,7 +150,7 @@ class PatuParser(object):
if matchobj:
break
if not matchobj:
print " **** failed to match line '%s'" % (line)
print(" **** failed to match line '%s'" % (line))
return
# Strip strings
matchdict = matchobj.groupdict()
@@ -146,7 +161,8 @@ class PatuParser(object):
del matchdict[field]
matchkeys = set(matchdict.keys())
needstrip = set(["bankcontact1", "bankcontact2", "bankcontact3",
needstrip = set([
"bankcontact1", "bankcontact2", "bankcontact3",
"customerid", "accountowner", "accountname", "refnr", "formnr",
"recipientname", "eventdesc", "recipientaccount", "message",
"principalinfo1", "bankinfo1", "bankinfo2", "bankinfo3",
@@ -158,30 +174,35 @@ class PatuParser(object):
for field in matchkeys & needstrip:
matchdict[field] = matchdict[field].strip()
# Convert to int
needsint = set(["itemcount", "eventid", "record_len",
needsint = set([
"itemcount", "eventid", "record_len",
"depositcount", "withdrawcount"])
for field in matchkeys & needsint:
matchdict[field] = float(matchdict[field])
# Convert to float
needsfloat = set(["startingbalance", "accountlimit", "amount",
needsfloat = set([
"startingbalance", "accountlimit", "amount",
"destinationamount", "balance", "availablefunds", "depositsum",
"withdrawsum", "avgbalance", "avglimitbalance",
"permanentbalance"])
for field in matchkeys & needsfloat:
matchdict[field] = float(matchdict[field])
# convert sents to euros
needseur = set(["startingbalance", "accountlimit", "amount",
needseur = set([
"startingbalance", "accountlimit", "amount",
"destinationamount", "balance", "availablefunds", "depositsum",
"withdrawsum", "avgbalance", "permanentbalance"])
for field in matchkeys & needseur:
matchdict[field] = matchdict[field] / 100
# convert ibanswift to separate fields
if matchdict.has_key("ibanswift"):
matchdict["iban"], matchdict["swift"] = \
matchdict["ibanswift"].strip().split()
if "ibanswift" in matchdict:
matchdict["iban"], matchdict["swift"] = (
matchdict["ibanswift"].strip().split()
)
# Convert date fields
needdate = set(["startdate", "enddate", "creationdate", "balancedate",
needdate = set([
"startdate", "enddate", "creationdate", "balancedate",
"valuedate", "paymentdate", "recorddate", "perioddate"])
for field in matchkeys & needdate:
# Base all dates on the year 2000, since it's unlikely that this
@@ -191,17 +212,20 @@ class PatuParser(object):
matchdict[field] = None
continue
matchdict[field] = datetime.date(int("20" + datestring[0:2]),
int(datestring[2:4]), int(datestring[4:6]))
matchdict[field] = datetime.date(
int("20" + datestring[0:2]),
int(datestring[2:4]), int(datestring[4:6]))
# convert time fields
needtime = set(["creationtime"])
for field in matchkeys & needtime:
timestring = matchdict[field]
matchdict[field] = datetime.time(int(timestring[0:2]),
int(timestring[2:4]))
matchdict[field] = datetime.time(
int(timestring[0:2]),
int(timestring[2:4]))
return matchdict
def parse_file(filename):
"""Parse file with PATU format inside
@@ -214,6 +238,7 @@ def parse_file(filename):
for line in patufile:
parser.parse_record(line)
def main():
"""The main function, currently just calls a dummy filename
@@ -223,5 +248,3 @@ def main():
if __name__ == '__main__':
main()

View File

@@ -6,8 +6,8 @@
# 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
# 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,
@@ -30,20 +30,23 @@ from account_banking_fi_patu.parser import PatuParser
__all__ = ['parser']
class transaction(models.mem_bank_transaction):
'''
Implementation of transaction communication class for account_banking.
'''
mapping = {
"remote_account": "recipientaccount",
"remote_currency": "currency",
"transferred_amount": "amount",
"execution_date": "recorddate",
"value_date": "paymentdate",
"transfer_type": "eventtype",
"reference": "refnr",
"eventcode": "eventcode",
"message": "message"}
"remote_account": "recipientaccount",
"remote_currency": "currency",
"transferred_amount": "amount",
"execution_date": "recorddate",
"value_date": "paymentdate",
"transfer_type": "eventtype",
"reference": "refnr",
"eventcode": "eventcode",
"message": "message"
}
def __init__(self, record, *args, **kwargs):
'''
Initialize own dict with read values.
@@ -63,19 +66,19 @@ class transaction(models.mem_bank_transaction):
If eventcode is 730, the transaction was initiated by the bank and
doesn't have a destination account.
'''
if self.eventcode and (self.eventcode == "720" or self.eventcode ==
"710"):
if self.eventcode in ["720", "710"]:
# Withdrawal from and deposit to the account
return (self.execution_date and self.transferred_amount and True) \
or False
or False
if self.eventcode and self.eventcode == "730":
# The transaction is bank initiated, no remote account is present
return (self.execution_date and self.transferred_amount and True) \
or False
or False
return super(transaction, self).is_valid()
class statement(models.mem_bank_statement):
'''
Implementation of bank_statement communication class of account_banking
@@ -110,6 +113,7 @@ class statement(models.mem_bank_statement):
return
self.transactions.append(transaction(record))
class parser(models.parser):
code = 'FIPATU'
name = _('PATU statement sheet')

View File

@@ -43,11 +43,19 @@
French Letter of Change
=======================
This module adds support for French Letters of Change (in French : Lettre de Change Relevé aka LCR). This payment type is still in use in France and it is *not* replaced by SEPA one-off Direct Debits. With this module, you can generate a CFONB file to send to your bank.
This module adds support for French Letters of Change (in French :
Lettre de Change Relevé aka LCR).
This module uses the framework provided by the banking addons, cf https://launchpad.net/banking-addons
This payment type is still in use in France and it is *not* replaced by SEPA
one-off Direct Debits.
Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com> for any help or question about this module.
''',
With this module, you can generate a CFONB file to send to your bank.
This module uses the framework provided by the banking addons,
cf https://github.com/OCA/banking
Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com>
for any help or question about this module.
''',
'active': False,
}

View File

@@ -23,10 +23,14 @@ This module provides online bank databases for conversion between BBAN and
IBAN numbers and for consulting.
'''
import re
import urllib, urllib2
import urllib
import urllib2
from BeautifulSoup import BeautifulSoup
from openerp.addons.account_banking.sepa import postalcode
from openerp.addons.account_banking_iban_lookup.urlagent import URLAgent, SoupForm
from openerp.addons.account_banking_iban_lookup.urlagent import (
URLAgent,
SoupForm,
)
from openerp.addons.account_banking.sepa.iban import IBAN
from openerp.addons.account_banking.struct import struct
@@ -38,6 +42,7 @@ __all__ = [
IBANlink_NL = 'http://www.ibannl.org/iban_check.php'
IBANlink_BE = 'http://www.ibanbic.be/'
def get_iban_bic_NL(bank_acc):
'''
Consult the Dutch online banking database to check both the account number
@@ -51,14 +56,14 @@ def get_iban_bic_NL(bank_acc):
if len(number) <= 7:
iban = IBAN.create(BBAN='INGB' + number.rjust(10, '0'),
countrycode='NL'
)
)
return struct(
iban = iban.replace(' ',''),
account = iban.BBAN[4:],
bic = 'INGBNL2A',
code = 'INGBNL',
bank = 'ING Bank N.V.',
country_id = 'NL',
iban=iban.replace(' ', ''),
account=iban.BBAN[4:],
bic='INGBNL2A',
code='INGBNL',
bank='ING Bank N.V.',
country_id='NL',
)
data = urllib.urlencode(dict(number=number, method='POST'))
@@ -66,6 +71,7 @@ def get_iban_bic_NL(bank_acc):
response = urllib2.urlopen(request)
soup = BeautifulSoup(response)
result = struct()
attr = None
for _pass, td in enumerate(soup.findAll('td')):
if _pass % 2 == 1:
result[attr] = unicode(td.find('font').contents[0])
@@ -81,6 +87,7 @@ def get_iban_bic_NL(bank_acc):
return result
return None
def get_iban_bic_BE(bank_acc):
'''
Consult the Belgian online database to check both account number and the
@@ -88,7 +95,9 @@ def get_iban_bic_BE(bank_acc):
in Belgium and will only convert Belgian local account numbers.
'''
def contents(soup, attr):
return soup.find('input', {'id': 'textbox%s' % attr}).get('value').strip()
return soup.find('input', {
'id': 'textbox%s' % attr
}).get('value').strip()
if not bank_acc.strip():
return None
@@ -121,6 +130,7 @@ def get_iban_bic_BE(bank_acc):
result.code = result.bic[:6]
return result
def BBAN_is_IBAN(bank_acc):
'''
Intelligent copy, valid for SEPA members who switched to SEPA from old
@@ -131,15 +141,16 @@ def BBAN_is_IBAN(bank_acc):
else:
iban_acc = IBAN(bank_acc)
return struct(
iban = str(iban_acc),
account = str(bank_acc),
country_id = iban_acc.countrycode,
code = iban_acc.BIC_searchkey,
iban=str(iban_acc),
account=str(bank_acc),
country_id=iban_acc.countrycode,
code=iban_acc.BIC_searchkey,
# Note: BIC can not be constructed here!
bic = False,
bank = False,
bic=False,
bank=False,
)
_account_info = {
# TODO: Add more online data banks
'BA': BBAN_is_IBAN,
@@ -153,6 +164,7 @@ _account_info = {
'SM': BBAN_is_IBAN,
}
def account_info(iso, bank_acc):
'''
Consult the online database for this country to obtain its
@@ -165,9 +177,11 @@ def account_info(iso, bank_acc):
return _account_info[iso](bank_acc)
return False
bic_re = re.compile("[^']+'([^']*)'.*")
SWIFTlink = 'http://www.swift.com/bsl/freequery.do'
def bank_info(bic):
'''
Consult the free online SWIFT service to obtain the name and address of a
@@ -177,7 +191,7 @@ def bank_info(bic):
automated usage, so user like behavior is required.
Update January 2012: Always return None, as the SWIFT page to retrieve the
information does no longer exist.
information does no longer exist.
If demand exists, maybe bite the bullet and integrate with a paid web
service such as http://www.iban-rechner.de.
lp914922 additionally suggests to make online lookup optional.
@@ -190,7 +204,7 @@ def bank_info(bic):
for trsoup in soup('tr'):
for stage, tdsoup in enumerate(trsoup('td')):
if stage == 0:
attr = tdsoup.contents[0].strip().replace(' ','_')
attr = tdsoup.contents[0].strip().replace(' ', '_')
elif stage == 2:
if tdsoup.contents:
retval[attr] = tdsoup.contents[0].strip()
@@ -203,8 +217,8 @@ def bank_info(bic):
request = agent.open(SWIFTlink)
soup = BeautifulSoup(request)
# Parse request form. As this form is intertwined with a table, use the parent
# as root to search for form elements.
# Parse request form. As this form is intertwined with a table, use the
# parent as root to search for form elements.
form = SoupForm(soup.find('form', {'id': 'frmFreeSearch1'}), parent=True)
# Fill form fields
@@ -221,7 +235,8 @@ def bank_info(bic):
if not bic_button:
return None, None
# Overwrite the location with 'any' ('XXX') to narrow the results to one or less.
# Overwrite the location with 'any' ('XXX') to narrow the results to one
# or less.
# Assume this regexp will never fail...
full_bic = bic_re.match(bic_button.get('href')).groups()[0][:8] + 'XXX'
@@ -236,13 +251,13 @@ def bank_info(bic):
soup = BeautifulSoup(response)
# Now parse the results
tables = soup.find('div', {'id':'Middle'}).findAll('table')
tables = soup.find('div', {'id': 'Middle'}).findAll('table')
if not tables:
return None, None
tablesoup = tables[2]('table')
if not tablesoup:
return None, None
codes = harvest(tablesoup[0])
if not codes:
return None, None
@@ -253,9 +268,9 @@ def bank_info(bic):
# banks world wide using the same name.
# The concatenation with the two character country code is for most
# national branches sufficient as a unique identifier.
code = full_bic[:6],
bic = full_bic,
name = codes.Institution_name,
code=full_bic[:6],
bic=full_bic,
name=codes.Institution_name,
)
address = harvest(tablesoup[1])
@@ -264,14 +279,14 @@ def bank_info(bic):
if not address.Zip_Code:
if address.Location:
iso, address.Zip_Code, address.Location = \
postalcode.split(address.Location, full_bic[4:6])
postalcode.split(address.Location, full_bic[4:6])
bankaddress = struct(
street = address.Address.title(),
city = address.Location.strip().title(),
zip = address.Zip_Code,
country = address.Country.title(),
country_id = full_bic[4:6],
street=address.Address.title(),
city=address.Location.strip().title(),
zip=address.Zip_Code,
country=address.Country.title(),
country_id=full_bic[4:6],
)
if ' ' in bankaddress.street:
bankaddress.street, bankaddress.street2 = [
@@ -281,4 +296,3 @@ def bank_info(bic):
bankaddress.street2 = ''
return bankinfo, bankaddress

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -28,7 +28,8 @@ import urllib
__all__ = ['urlsplit', 'urljoin', 'pathbase', 'urlbase', 'SoupForm',
'URLAgent'
]
]
def urlsplit(url):
'''
@@ -43,7 +44,8 @@ def urlsplit(url):
host, path = urllib.splithost(url)
return (scheme, host, path)
def urljoin(scheme, host, path, args = None):
def urljoin(scheme, host, path, args=None):
'''
Join scheme, host and path to a full URL.
Optional: add urlencoded args.
@@ -54,15 +56,17 @@ def urljoin(scheme, host, path, args = None):
url += '?%s' % urllib.urlencode(args)
return url
def pathbase(path):
'''
Return the base for the path in order to satisfy relative paths.
Helper function.
'''
if path and '/' in path:
return path[:path.rfind('/') +1]
return path[:path.rfind('/') + 1]
return path
def urlbase(url):
'''
Return the base URL for url in order to satisfy relative paths.
@@ -71,6 +75,7 @@ def urlbase(url):
scheme, host, path = urlsplit(url)
return urljoin(scheme, host, pathbase(path))
class SoupForm(object):
'''
A SoupForm is a representation of a HTML Form in BeautifulSoup terms.
@@ -94,7 +99,7 @@ class SoupForm(object):
if parent:
self.soup = soup.parent
# Harvest input elements.
# Harvest input elements.
self._args = {}
for item in self.soup.findAll('input'):
# Make sure to initialize to '' to avoid None strings to appear
@@ -150,6 +155,7 @@ class SoupForm(object):
args.update(self._extra_args)
return args
class URLAgent(object):
'''
Assistent object to ease HTTP(S) requests.
@@ -160,8 +166,12 @@ class URLAgent(object):
super(URLAgent, self).__init__(*args, **kwargs)
self._extra_headers = {}
self.headers = {
'User-Agent': 'Mozilla/5.0 (X11; U; Linux x86_64; us; rv:1.9.0.10) Gecko/2009042708 Fedora/3.0.10-1.fc9 Firefox/3.0.10',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'User-Agent': (
'Mozilla/5.0 (X11; U; Linux x86_64; us; rv:1.9.0.10) '
'Gecko/2009042708 Fedora/3.0.10-1.fc9 Firefox/3.0.10'),
'Accept': (
'text/html,application/xhtml+xml,application/xml;'
'q=0.9,*/*;q=0.8'),
'Accept-Language': 'en-us;q=1.0',
'Accept-Charset': 'UTF-8,*',
'Cache-Control': 'max-age=0'
@@ -193,7 +203,7 @@ class URLAgent(object):
# Get and set cookies for next actions
attributes = request.info()
if attributes.has_key('set-cookie'):
if 'set-cookie' in attributes:
self.agent.addheader('Cookie', attributes['set-cookie'])
# Add referer

View File

@@ -19,9 +19,9 @@
#
##############################################################################
{
"name" : "MT940",
"version" : "1.0",
"author" : "Therp BV",
"name": "MT940",
"version": "1.0",
"author": "Therp BV",
"complexity": "expert",
"description": """
This addon provides a generic parser for MT940 files. Given that MT940 is a
@@ -32,11 +32,11 @@ certain bank.
See account_banking_nl_ing_mt940 for an example on how to use it.
""",
"category" : "Dependency",
"depends" : [
"category": "Dependency",
"depends": [
'account_banking',
],
"data" : [
"data": [
],
"js": [
],
@@ -47,7 +47,7 @@ See account_banking_nl_ing_mt940 for an example on how to use it.
"auto_install": False,
"installable": True,
"application": False,
"external_dependencies" : {
'python' : [],
"external_dependencies": {
'python': [],
},
}

View File

@@ -27,18 +27,23 @@ import re
import datetime
import logging
try:
from openerp.addons.account_banking.parsers.models import\
mem_bank_statement, mem_bank_transaction
from openerp.addons.account_banking.parsers.models import (
mem_bank_statement,
mem_bank_transaction,
)
from openerp.tools.misc import DEFAULT_SERVER_DATE_FORMAT
except ImportError:
#this allows us to run this file standalone, see __main__ at the end
# this allows us to run this file standalone, see __main__ at the end
class mem_bank_statement:
def __init__(self):
self.transactions = []
class mem_bank_transaction:
pass
DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
class MT940(object):
'''Inherit this class in your account_banking.parsers.models.parser,
define functions to handle the tags you need to handle and adjust static
@@ -46,7 +51,7 @@ class MT940(object):
Note that order matters: You need to do your_parser(MT940, parser), not the
other way around!
At least, you should override handle_tag_61 and handle_tag_86. Don't forget
to call super.
handle_tag_* functions receive the remainder of the the line (that is,
@@ -59,7 +64,7 @@ class MT940(object):
footer_regex = '^-}$'
footer_regex = '^-XXX$'
'The line that denotes end of message, we need to create a new statement'
tag_regex = '^:[0-9]{2}[A-Z]*:'
'The beginning of a record, should be anchored to beginning of the line'
@@ -194,15 +199,17 @@ class MT940(object):
banks occur'''
pass
'utility functions'
def str2date(string, fmt='%y%m%d'):
return datetime.datetime.strptime(string, fmt)
def str2float(string):
return float(string.replace(',', '.'))
'testing'
def main(filename):
"""testing"""
parser = MT940()
parser.parse(None, open(filename, 'r').read())
for statement in parser.statements:

View File

@@ -35,8 +35,8 @@ No formal specifications of the file layout are released by abnamro. You can
help improve the performance of this import filter on
https://launchpad.net/account-banking.
Imported bank transfers are organized in statements covering periods of one week,
even if the imported files cover a different period.
''',
Imported bank transfers are organized in statements covering periods of one
week, even if the imported files cover a different period.
''',
'installable': True,
}

View File

@@ -31,7 +31,6 @@ Bank Statements along with Bank Transactions.
'''
from openerp.addons.account_banking.parsers import models
from openerp.addons.account_banking.parsers.convert import str2date
from openerp.addons.account_banking.sepa import postalcode
from openerp.tools.translate import _
from openerp.osv import orm
@@ -42,6 +41,7 @@ __all__ = ['parser']
bt = models.mem_bank_transaction
class transaction_message(object):
'''
A auxiliary class to validate and coerce read values
@@ -56,9 +56,10 @@ class transaction_message(object):
Initialize own dict with attributes and coerce values to right type
'''
if len(self.attrnames) != len(values):
raise ValueError, \
_('Invalid transaction line: expected %d columns, found '
'%d') % (len(self.attrnames), len(values))
raise ValueError(
_('Invalid transaction line: expected %d columns, found '
'%d') % (len(self.attrnames), len(values))
)
''' Strip all values except the blob '''
for (key, val) in zip(self.attrnames, values):
self.__dict__[key] = key == 'blob' and val or val.strip()
@@ -72,23 +73,24 @@ class transaction_message(object):
self.statement_id = self.execution_date.strftime('%Yw%W')
self.id = str(subno).zfill(4)
class transaction(models.mem_bank_transaction):
'''
Implementation of transaction communication class for account_banking.
'''
attrnames = ['local_account', 'local_currency', 'transferred_amount',
'blob', 'execution_date', 'value_date', 'id',
]
]
type_map = {
# retrieved from online help in the Triodos banking application
'BEA': bt.PAYMENT_TERMINAL, # Pin
'GEA': bt.BANK_TERMINAL, # ATM
'BEA': bt.PAYMENT_TERMINAL, # Pin
'GEA': bt.BANK_TERMINAL, # ATM
'COSTS': bt.BANK_COSTS,
'BANK': bt.ORDER,
'GIRO': bt.ORDER,
'INTL': bt.ORDER, # international order
'UNKN': bt.ORDER, # everything else
'INTL': bt.ORDER, # international order
'UNKN': bt.ORDER, # everything else
'SEPA': bt.ORDER,
'PAYB': bt.PAYMENT_BATCH,
'RETR': bt.STORNO,
@@ -118,10 +120,10 @@ class transaction(models.mem_bank_transaction):
elif not self.execution_date:
self.error_message = "No execution date"
elif not self.remote_account and self.transfer_type not in [
'BEA', 'GEA', 'COSTS', 'UNKN', 'PAYB',
]:
self.error_message = _('No remote account for transaction type '
'%s') % self.transfer_type
'BEA', 'GEA', 'COSTS', 'UNKN', 'PAYB', ]:
self.error_message = _(
'No remote account for transaction type %s'
) % self.transfer_type
if self.error_message:
raise orm.except_orm(_('Error !'), _(self.error_message))
return not self.error_message
@@ -139,12 +141,11 @@ class transaction(models.mem_bank_transaction):
res = []
while(len(line) > col * size):
separation = (col + 1) * size - 1
if line[col * size : separation].strip():
part = line[col * size : separation]
if line[col * size: separation].strip():
part = line[col * size: separation]
# If the separation character is not a space, add it anyway
# presumably for sepa feedback strings only
if (len(line) > separation
and line[separation] != ' '):
if (len(line) > separation and line[separation] != ' '):
part += line[separation]
res.append(part)
col += 1
@@ -180,7 +181,7 @@ class transaction(models.mem_bank_transaction):
end_index = start_index + 1
while end_index < items_len:
key = '/'.join(items[start_index:end_index])
if key in known_keys:
if key in known_keys:
return (key, start_index, end_index)
end_index += 1
start_index += 1
@@ -203,15 +204,16 @@ class transaction(models.mem_bank_transaction):
key_info = _get_next_key(items, item_index)
value_end_index = (key_info and key_info[1]) or items_len
sepa_value = (
((value_end_index > item_index)
and '/'.join(items[item_index:value_end_index]))
(
(value_end_index > item_index)
and '/'.join(items[item_index:value_end_index]))
or '')
sepa_dict[sepa_key] = sepa_value
return sepa_dict
def parse_type(field):
# here we process the first field, which identifies the statement type
# and in case of certain types contains additional information
# here we process the first field, which identifies the statement
# type and in case of certain types contains additional information
transfer_type = 'UNKN'
remote_account = False
remote_owner = False
@@ -233,12 +235,14 @@ class transaction(models.mem_bank_transaction):
transfer_type = 'BEA'
# columns 6 to 16 contain the terminal identifier
# column 17 contains a space
# columns 18 to 31 contain date and time in DD.MM.YY/HH.MM format
elif field.startswith('GEA '):
# columns 18 to 31 contain date and time in DD.MM.YY/HH.MM
# format
elif field.startswith('GEA '):
transfer_type = 'GEA'
# columns 6 to 16 contain the terminal identifier
# column 17 contains a space
# columns 18 to 31 contain date and time in DD.MM.YY/HH.MM format
# columns 18 to 31 contain date and time in DD.MM.YY/HH.MM
# format
elif field.startswith('MAANDBIJDRAGE ABNAMRO'):
transfer_type = 'COSTS'
elif re.match("^\s([0-9]+\.){3}[0-9]+\s", field):
@@ -251,9 +255,10 @@ class transaction(models.mem_bank_transaction):
elif field.startswith("TOTAAL BETALINGEN"):
transfer_type = 'PAYB'
return (transfer_type, remote_account, remote_owner)
fields = split_blob(self.blob)
(self.transfer_type, self.remote_account, self.remote_owner) = parse_type(fields[0])
(self.transfer_type, self.remote_account, self.remote_owner) = \
parse_type(fields[0])
if self.transfer_type == 'SEPA':
sepa_dict = get_sepa_dict(''.join(fields))
@@ -263,7 +268,7 @@ class transaction(models.mem_bank_transaction):
'SEPA BATCH SALARIS': 'PAYB',
'SEPA TERUGBOEKING': 'RETR',
}.get(sepa_type.upper(), 'SEPA')
self.remote_account = sepa_dict.get('IBAN',False)
self.remote_account = sepa_dict.get('IBAN', False)
self.remote_bank_bic = sepa_dict.get('BIC', False)
self.remote_owner = sepa_dict.get('NAME', False)
self.reference = sepa_dict.get('REMI', '')
@@ -278,21 +283,24 @@ class transaction(models.mem_bank_transaction):
elif self.transfer_type == 'BEA':
# second column contains remote owner and bank pass identification
self.remote_owner = len(fields) > 1 and fields[1].split(',')[0].strip() or False
# column 2 and up can contain additional messsages
self.remote_owner = (
len(fields) > 1 and fields[1].split(',')[0].strip() or False)
# column 2 and up can contain additional messsages
# (such as transaction costs or currency conversion)
self.message = ' '.join(field.strip() for field in fields)
elif self.transfer_type == 'BANK':
# second column contains the remote owner or the first message line
if not self.remote_owner:
self.remote_owner = len(fields) > 1 and fields[1].strip() or False
self.remote_owner = (
len(fields) > 1 and fields[1].strip() or False)
self.message = ' '.join(field.strip() for field in fields[2:])
else:
self.message = ' '.join(field.strip() for field in fields[1:])
elif self.transfer_type == 'INTL':
# first column seems to consist of some kind of international transaction id
# first column seems to consist of some kind of international
# transaction id
self.reference = fields[0].strip()
# second column seems to contain remote currency and amount
# to be processed in a later release of this module
@@ -317,12 +325,14 @@ class transaction(models.mem_bank_transaction):
# but can be any numeric line really
for field in fields[1:]:
m = re.match(
"^\s*((BETALINGSKENM\.)|(ACCEPTGIRO))?\s*([0-9]+([ /][0-9]+)*)\s*$",
"^\s*((BETALINGSKENM\.)|(ACCEPTGIRO))?\s*([0-9]+"
"([ /][0-9]+)*)\s*$",
field)
if m:
self.reference = m.group(4)
break
class statement(models.mem_bank_statement):
'''
Implementation of bank_statement communication class of account_banking
@@ -335,7 +345,7 @@ class statement(models.mem_bank_statement):
self.id = msg.statement_id
self.local_account = msg.local_account
self.date = str2date(msg.date, '%Y%m%d')
self.start_balance = self.end_balance = 0 # msg.start_balance
self.start_balance = self.end_balance = 0 # msg.start_balance
self.import_transaction(msg)
def import_transaction(self, msg):
@@ -346,6 +356,7 @@ class statement(models.mem_bank_statement):
self.end_balance += trans.transferred_amount
self.transactions.append(trans)
class parser(models.parser):
code = 'ABNAM'
country_code = 'NL'
@@ -365,7 +376,7 @@ each file covers a period of two weeks.
# Transaction lines are not numbered, so keep a tracer
subno = 0
statement_id = False
for line in csv.reader(lines, delimiter = '\t', quoting=csv.QUOTE_NONE):
for line in csv.reader(lines, delimiter='\t', quoting=csv.QUOTE_NONE):
# Skip empty (last) lines
if not line:
continue
@@ -381,5 +392,3 @@ each file covers a period of two weeks.
stmnt = statement(msg)
result.append(stmnt)
return result
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -11,8 +11,8 @@
# garantees and support are strongly adviced to contract EduSense BV
#
# 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
# 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,
@@ -24,7 +24,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import wizard
import account_banking_nl_clieop
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import wizard
from . import account_banking_nl_clieop

View File

@@ -4,8 +4,8 @@
# 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
# 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,

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -19,7 +19,6 @@
#
##############################################################################
from datetime import date
from openerp.osv import orm, fields
from openerp.tools.translate import _
@@ -45,7 +44,7 @@ class clieop_export(orm.Model):
'duplicates':
fields.integer('Number of Duplicates', readonly=True),
'prefered_date':
fields.date('Prefered Processing Date',readonly=True),
fields.date('Prefered Processing Date', readonly=True),
'no_transactions':
fields.integer('Number of Transactions', readonly=True),
'check_no_accounts':
@@ -81,13 +80,13 @@ class clieop_export(orm.Model):
'''
last = 1
last_ids = self.search(cr, uid, [
('date_generated', '=',
fields.date.context_today(self, cr,uid,context))
], context=context)
('date_generated', '=', fields.date.context_today(
self, cr, uid, context)),
], context=context)
if last_ids:
last = 1 + max([x['daynumber'] for x in self.read(
cr, uid, last_ids, ['daynumber'],
context=context)])
cr, uid, last_ids, ['daynumber'], context=context)]
)
return last
_defaults = {

View File

@@ -4,8 +4,8 @@
# Copyright (C) 2011 Therp BV (<http://therp.nl>)
#
# 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
# 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,
@@ -20,25 +20,27 @@
""" This script covers the migration of the payment wizards from old style to
new style (osv_memory). It guarantees an easy upgrade for early adopters
of the 6.0 branch of this OpenERP module. Note that a migration from OpenERP
of the 6.0 branch of this OpenERP module. Note that a migration from OpenERP
5.0 to OpenERP 6.0 with respect to this module is not covered by this script.
"""
__name__ = "payment.mode.type:: Add new style payment wizards to existing payment mode types"
__name__ = ("payment.mode.type:: Add new style payment wizards to existing "
"payment mode types")
def migrate(cr, version):
cr.execute ("UPDATE payment_mode_type"
" SET ir_model_id = data1.res_id"
" FROM ir_model_data data1,"
" ir_model_data data2"
" WHERE data2.res_id = payment_mode_type.id"
" AND data1.module = 'account_banking_nl_clieop'"
" AND data1.model = 'ir.model'"
" AND data1.name = 'model_banking_export_clieop_wizard'"
" AND data2.module = 'account_banking_nl_clieop'"
" AND data2.model = 'payment.mode.type'"
" AND data2.name IN ('export_clieop_inc',"
" 'export_clieop_pay',"
" 'export_clieop_sal'"
" )"
)
cr.execute("UPDATE payment_mode_type"
" SET ir_model_id = data1.res_id"
" FROM ir_model_data data1,"
" ir_model_data data2"
" WHERE data2.res_id = payment_mode_type.id"
" AND data1.module = 'account_banking_nl_clieop'"
" AND data1.model = 'ir.model'"
" AND data1.name = 'model_banking_export_clieop_wizard'"
" AND data2.module = 'account_banking_nl_clieop'"
" AND data2.model = 'payment.mode.type'"
" AND data2.name IN ('export_clieop_inc',"
" 'export_clieop_pay',"
" 'export_clieop_sal'"
" )"
)

View File

@@ -4,8 +4,8 @@
# Copyright (C) 2011 Therp BV (<http://therp.nl>)
#
# 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
# 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,
@@ -18,16 +18,19 @@
#
##############################################################################
""" r64: introduction of the payment_mode_type in order to support of debit orders
"""r64: introduction of the payment_mode_type in order to support of debit
orders
"""
__name__ = "payment.mode.type:: set payment_mode_type to 'debit' for Clieop incasso export"
__name__ = ("payment.mode.type:: set payment_mode_type to 'debit' for Clieop "
"incasso export")
def migrate(cr, version):
cr.execute ("UPDATE payment_mode_type"
" SET payment_order_type = 'debit'"
" FROM ir_model_data "
" WHERE res_id = payment_mode_type.id"
" AND module = 'account_banking_nl_clieop'"
" AND model = 'payment.mode.type'"
" AND ir_model_data.name = 'export_clieop_inc'"
)
cr.execute("UPDATE payment_mode_type"
" SET payment_order_type = 'debit'"
" FROM ir_model_data "
" WHERE res_id = payment_mode_type.id"
" AND module = 'account_banking_nl_clieop'"
" AND model = 'payment.mode.type'"
" AND ir_model_data.name = 'export_clieop_inc'"
)

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -18,6 +18,5 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import export_clieop
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import export_clieop

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -26,7 +26,8 @@ __all__ = ['DirectDebitBatch', 'PaymentsBatch', 'DirectDebit', 'Payment',
'DirectDebitFile', 'PaymentsFile', 'SalaryPaymentsFile',
'SalaryPaymentOrder', 'PaymentOrder', 'DirectDebitOrder',
'OrdersFile',
]
]
class SWIFTField(record.Field):
'''
@@ -37,27 +38,24 @@ class SWIFTField(record.Field):
kwargs['cast'] = convert.to_swift
super(SWIFTField, self).__init__(*args, **kwargs)
#def take(self, buffer):
# return convert.to_swift(super(SWIFTField, self).take(buffer))
#def format(self, value):
# return convert.to_swift(super(SWIFTField, self).format(value))
class SWIFTFieldNoLeadingWhitespace(SWIFTField):
def format(self, value):
return super(SWIFTFieldNoLeadingWhitespace, self).format(
self.cast(value).lstrip())
def eleven_test(s):
'''
Dutch eleven-test for validating 9-long local bank account numbers.
'''
r = 0
l = len(s)
for i,c in enumerate(s):
for i, c in enumerate(s):
r += (l-i) * int(c)
return (r % 11) == 0
def chunk(str, length):
'''
Split a string in equal sized substrings of length <length>
@@ -66,7 +64,8 @@ def chunk(str, length):
yield str[:length]
str = str[length:]
class HeaderRecord(record.Record): #{{{
class HeaderRecord(record.Record):
'''ClieOp3 header record'''
_fields = [
record.Filler('recordcode', 4, '0001'),
@@ -84,7 +83,7 @@ class HeaderRecord(record.Record): #{{{
self.sender_id = id or ''
self.file_id = '%02d%02d' % (self.creation_date.day, seqno)
self.duplicatecode = duplicate and '2' or '1'
#}}}
class FooterRecord(record.Record):
'''ClieOp3 footer record'''
@@ -94,6 +93,7 @@ class FooterRecord(record.Record):
record.Filler('filler', 45),
]
class BatchHeaderRecord(record.Record):
'''Header record preceding new batches'''
_fields = [
@@ -107,6 +107,7 @@ class BatchHeaderRecord(record.Record):
record.Filler('filler', 10),
]
class BatchFooterRecord(record.Record):
'''Closing record for batches'''
_fields = [
@@ -118,6 +119,7 @@ class BatchFooterRecord(record.Record):
record.Filler('filler', 10),
]
class FixedMessageRecord(record.Record):
'''Fixed message'''
_fields = [
@@ -127,6 +129,7 @@ class FixedMessageRecord(record.Record):
record.Filler('filler', 13),
]
class SenderRecord(record.Record):
'''Ordering party'''
_fields = [
@@ -140,6 +143,7 @@ class SenderRecord(record.Record):
record.Filler('filler', 2),
]
class TransactionRecord(record.Record):
'''Transaction'''
_fields = [
@@ -152,6 +156,7 @@ class TransactionRecord(record.Record):
record.Filler('filler', 9),
]
class NamePayerRecord(record.Record):
'''Name payer'''
_fields = [
@@ -161,6 +166,7 @@ class NamePayerRecord(record.Record):
record.Filler('filler', 10),
]
class PaymentReferenceRecord(record.Record):
'''Payment reference'''
_fields = [
@@ -170,6 +176,7 @@ class PaymentReferenceRecord(record.Record):
record.Filler('filler', 29),
]
class DescriptionRecord(record.Record):
'''Description'''
_fields = [
@@ -179,6 +186,7 @@ class DescriptionRecord(record.Record):
record.Filler('filler', 13),
]
class NameBeneficiaryRecord(record.Record):
'''Name receiving party'''
_fields = [
@@ -188,6 +196,7 @@ class NameBeneficiaryRecord(record.Record):
record.Filler('filler', 10),
]
class OrderRecord(record.Record):
'''Order details'''
_fields = [
@@ -203,23 +212,28 @@ class OrderRecord(record.Record):
record.Filler('currency', 3, 'EUR'),
record.Field('testcode', 1),
]
def __init__(self, *args, **kwargs):
super(OrderRecord, self).__init__(*args, **kwargs)
self.batch_medium = 'DATACOM'
self.name_transactioncode = self._transactioncode
class SalaryPaymentOrder(OrderRecord):
'''Salary payment batch record'''
_transactioncode = 'SALARIS'
class PaymentOrder(OrderRecord):
'''Payment batch record'''
_transactioncode = 'CREDBET'
class DirectDebitOrder(OrderRecord):
'''Direct debit payments batch record'''
_transactioncode = 'INCASSO'
class Optional(object):
'''Auxilliary class to handle optional records'''
def __init__(self, klass, max=1):
@@ -233,7 +247,7 @@ class Optional(object):
super(Optional, self).__setattr__(attr, value)
else:
if self._guts and len(self._guts) > self._max:
raise ValueError, 'Only %d lines are allowed' % self._max
raise ValueError('Only %d lines are allowed' % self._max)
newitem = self._klass()
setattr(newitem, attr, value)
self._guts.append(newitem)
@@ -259,6 +273,7 @@ class Optional(object):
'''Make sure to adapt'''
return self._guts.__iter__()
class OrdersFile(object):
'''A payment orders file'''
def __init__(self, *args, **kwargs):
@@ -271,18 +286,19 @@ class OrdersFile(object):
'''
return '\r\n'.join(self.orders)
class Transaction(object):
'''Generic transaction class'''
def __init__(self, type_=0, name=None, reference=None, messages=[],
accountno_beneficiary=None, accountno_payer=None,
amount=0
):
amount=0):
self.transaction = TransactionRecord()
self.paymentreference = Optional(PaymentReferenceRecord)
self.description = Optional(DescriptionRecord, 4)
self.transaction.transactiontype = type_
# Remove Postbank account marker 'P'
self.transaction.accountno_beneficiary = accountno_beneficiary.replace('P', '0')
self.transaction.accountno_beneficiary = accountno_beneficiary.replace(
'P', '0')
self.transaction.accountno_payer = accountno_payer.replace('P', '0')
self.transaction.amount = int(round(amount * 100))
if reference:
@@ -290,8 +306,7 @@ class Transaction(object):
# Allow long message lines to redistribute over multiple message
# records
for msg in chunk(''.join(messages),
self.description.length('description')
):
self.description.length('description')):
try:
self.description.description = msg
except ValueError:
@@ -321,13 +336,14 @@ class DirectDebit(Transaction):
items.append(str(description))
return '\r\n'.join(items)
class Payment(Transaction):
'''Payment transaction'''
def __init__(self, *args, **kwargs):
reknr = kwargs['accountno_beneficiary']
if len(reknr.lstrip('0')) > 7:
if not eleven_test(reknr):
raise ValueError, '%s is not a valid bank account' % reknr
raise ValueError('%s is not a valid bank account' % reknr)
kwargs['type_'] = 5
self.name = NameBeneficiaryRecord()
super(Payment, self).__init__(*args, **kwargs)
@@ -346,6 +362,7 @@ class Payment(Transaction):
items.append(str(self.name))
return '\r\n'.join(items)
class SalaryPayment(Payment):
'''Salary Payment transaction'''
def __init__(self, *args, **kwargs):
@@ -353,14 +370,14 @@ class SalaryPayment(Payment):
kwargs['type_'] = len(reknr.lstrip('0')) <= 7 and 3 or 8
super(SalaryPayment, self).__init__(*args, **kwargs)
class Batch(object):
'''Generic batch class'''
transactionclass = None
def __init__(self, sender, rekeningnr, execution_date=None,
test=True, messages=[], transactiongroup=None,
batch_tracer=1, batch_id=''
):
batch_tracer=1, batch_id=''):
self.header = BatchHeaderRecord()
self.fixed_message = Optional(FixedMessageRecord, 4)
self.sender = SenderRecord()
@@ -386,18 +403,16 @@ class Batch(object):
@property
def total_amount(self):
'''total amount transferred'''
return reduce(lambda x,y: x + int(y.transaction.amount),
self.transactions, 0
)
return reduce(lambda x, y: x + int(y.transaction.amount),
self.transactions, 0)
@property
def total_accountnos(self):
'''check number on account numbers'''
return reduce(lambda x,y:
x + int(y.transaction.accountno_payer) + \
int(y.transaction.accountno_beneficiary),
self.transactions, 0
)
return reduce(lambda x, y:
x + int(y.transaction.accountno_payer) +
int(y.transaction.accountno_beneficiary),
self.transactions, 0)
@property
def rawdata(self):
@@ -423,18 +438,22 @@ class Batch(object):
self.transactions.append(retval)
return retval
class DirectDebitBatch(Batch):
'''Direct Debig Payment batch'''
transactionclass = DirectDebit
class PaymentsBatch(Batch):
'''Payment batch'''
transactionclass = Payment
class SalaryBatch(Batch):
'''Salary payment class'''
transactionclass = SalaryPayment
class ClieOpFile(object):
'''The grand unifying class'''
def __init__(self, identification='1', execution_date=None,
@@ -461,7 +480,7 @@ class ClieOpFile(object):
def batch(self, *args, **kwargs):
'''Create batch'''
kwargs['transactiongroup'] = self.transactiongroup
kwargs['batch_tracer'] = len(self.batches) +1
kwargs['batch_tracer'] = len(self.batches) + 1
kwargs['execution_date'] = self._execution_date
kwargs['test'] = self._test
args = (self._name_sender, self._accno_sender)
@@ -489,22 +508,22 @@ class ClieOpFile(object):
retval.total_accountnos = total_accountnos
return retval
class DirectDebitFile(ClieOpFile):
'''Direct Debit Payments file'''
transactiongroup = '10'
batchclass = DirectDebitBatch
orderclass = DirectDebitOrder
class PaymentsFile(ClieOpFile):
'''Payments file'''
transactiongroup = '00'
batchclass = PaymentsBatch
orderclass = PaymentOrder
class SalaryPaymentsFile(PaymentsFile):
'''Salary Payments file'''
batchclass = SalaryBatch
orderclass = SalaryPaymentOrder
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -6,8 +6,8 @@
# 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
# 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,
@@ -21,7 +21,7 @@
##############################################################################
import base64
from datetime import datetime, date, timedelta
from datetime import datetime, timedelta
from openerp.osv import orm, fields
from openerp.tools.translate import _
from openerp import netsvc
@@ -29,14 +29,17 @@ from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
from openerp.addons.account_banking import sepa
from openerp.addons.account_banking_nl_clieop.wizard import clieop
def strpdate(arg):
'''shortcut'''
return datetime.strptime(arg, DEFAULT_SERVER_DATE_FORMAT).date()
def strfdate(arg):
'''shortcut'''
return arg.strftime(DEFAULT_SERVER_DATE_FORMAT)
class banking_export_clieop_wizard(orm.TransientModel):
_name = 'banking.export.clieop.wizard'
_description = 'Client Opdrachten Export'
@@ -67,10 +70,9 @@ class banking_export_clieop_wizard(orm.TransientModel):
help=('This is the date the file should be processed by the bank. '
'Don\'t choose a date beyond the nearest date in your '
'payments. The latest allowed date is 30 days from now.\n'
'Please keep in mind that banks only execute on working days '
'and typically use a delay of two days between execution date '
'and effective transfer date.'
),
'Please keep in mind that banks only execute on working '
'days and typically use a delay of two days between '
'execution date and effective transfer date.'),
),
'test': fields.boolean(
'Test Run',
@@ -80,9 +82,8 @@ class banking_export_clieop_wizard(orm.TransientModel):
),
'fixed_message': fields.char(
'Fixed Message', size=32,
help=('A fixed message to apply to all transactions in addition to '
'the individual messages.'
),
help=('A fixed message to apply to all transactions in addition '
'to the individual messages.'),
),
# file fields
'file_id': fields.many2one(
@@ -90,7 +91,7 @@ class banking_export_clieop_wizard(orm.TransientModel):
'ClieOp File',
readonly=True
),
# fields.related does not seem to support
# fields.related does not seem to support
# fields of type selection
'testcode': fields.selection(
[('T', _('Yes')), ('P', _('No'))],
@@ -175,7 +176,9 @@ class banking_export_clieop_wizard(orm.TransientModel):
Also mind that rates for batches are way higher than those for
transactions. It pays to limit the number of batches.
'''
today = strpdate(fields.date.context_today(self, cr, uid, context=context))
today = strpdate(fields.date.context_today(
self, cr, uid, context=context
))
payment_order_obj = self.pool.get('payment.order')
# Payment order ids are provided in the context
@@ -199,12 +202,14 @@ class banking_export_clieop_wizard(orm.TransientModel):
elif payment_order.date_prefered == 'now':
execution_date = today
elif payment_order.date_prefered == 'due':
# Max processing date is 30 days past now, so limiting beyond that
# will catch too early payments
# Max processing date is 30 days past now, so limiting beyond
# that will catch too early payments
max_date = execution_date = today + timedelta(days=31)
for line in payment_order.line_ids:
if line.move_line_id.date_maturity:
date_maturity = strpdate(line.move_line_id.date_maturity)
date_maturity = strpdate(
line.move_line_id.date_maturity
)
if date_maturity < execution_date:
execution_date = date_maturity
else:
@@ -212,7 +217,8 @@ class banking_export_clieop_wizard(orm.TransientModel):
if execution_date and execution_date >= max_date:
raise orm.except_orm(
_('Error'),
_('You can\'t create ClieOp orders more than 30 days in advance.')
_('You can\'t create ClieOp orders more than 30 days '
'in advance.')
)
if len(runs) != 1:
raise orm.except_orm(
@@ -233,17 +239,19 @@ class banking_export_clieop_wizard(orm.TransientModel):
'''
Wizard to actually create the ClieOp3 file
'''
payment_order_obj = self.pool.get('payment.order')
clieop_export = self.browse(cr, uid, ids, context)[0]
clieopfile = None
for payment_order in clieop_export.payment_order_ids:
if not clieopfile:
# Just once: create clieop file
our_account_owner = payment_order.mode.bank_id.owner_name \
or payment_order.mode.bank_id.partner_id.name
our_account_owner = (
payment_order.mode.bank_id.owner_name
or payment_order.mode.bank_id.partner_id.name
)
if payment_order.mode.bank_id.state == 'iban':
our_account_nr = payment_order.mode.bank_id.acc_number_domestic
our_account_nr = (
payment_order.mode.bank_id.acc_number_domestic)
if not our_account_nr:
our_account_nr = sepa.IBAN(
payment_order.mode.bank_id.acc_number
@@ -253,21 +261,22 @@ class banking_export_clieop_wizard(orm.TransientModel):
if not our_account_nr:
raise orm.except_orm(
_('Error'),
_('Your bank account has to have a valid account number')
_('Your bank account has to have a valid account '
'number')
)
clieopfile = {'CLIEOPPAY': clieop.PaymentsFile,
'CLIEOPINC': clieop.DirectDebitFile,
'CLIEOPSAL': clieop.SalaryPaymentsFile,
}[clieop_export['batchtype']](
identification = clieop_export['reference'],
execution_date = clieop_export['execution_date'],
name_sender = our_account_owner,
accountno_sender = our_account_nr,
seqno = self.pool.get(
'banking.export.clieop').get_daynr(
cr, uid, context=context),
test = clieop_export['test']
)
clieopfile = {
'CLIEOPPAY': clieop.PaymentsFile,
'CLIEOPINC': clieop.DirectDebitFile,
'CLIEOPSAL': clieop.SalaryPaymentsFile,
}[clieop_export['batchtype']](
identification=clieop_export['reference'],
execution_date=clieop_export['execution_date'],
name_sender=our_account_owner,
accountno_sender=our_account_nr,
seqno=self.pool.get('banking.export.clieop').get_daynr(
cr, uid, context=context),
test=clieop_export['test']
)
# ClieOp3 files can contain multiple batches, but we put all
# orders into one single batch. Ratio behind this is that a
@@ -282,8 +291,8 @@ class banking_export_clieop_wizard(orm.TransientModel):
# The first payment order processed sets the reference of the
# batch.
batch = clieopfile.batch(
messages = messages,
batch_id = clieop_export['reference']
messages=messages,
batch_id=clieop_export['reference']
)
for line in payment_order.line_ids:
@@ -294,12 +303,13 @@ class banking_export_clieop_wizard(orm.TransientModel):
_('There is insufficient information.\r\n'
'Both destination address and account '
'number must be provided'
)
)
)
kwargs = dict(
name = line.bank_id.owner_name or line.bank_id.partner_id.name,
amount = line.amount_currency,
reference = line.communication or None,
name=line.bank_id.owner_name
or line.bank_id.partner_id.name,
amount=line.amount_currency,
reference=line.communication or None,
)
if line.communication2:
kwargs['messages'] = [line.communication2]
@@ -324,32 +334,32 @@ class banking_export_clieop_wizard(orm.TransientModel):
else:
kwargs['accountno_beneficiary'] = other_account_nr
kwargs['accountno_payer'] = our_account_nr
transaction = batch.transaction(**kwargs)
batch.transaction(**kwargs)
# Generate the specifics of this clieopfile
order = clieopfile.order
file_id = self.pool.get('banking.export.clieop').create(
cr, uid, dict(
filetype = order.name_transactioncode,
identification = order.identification,
prefered_date = strfdate(order.preferred_execution_date),
total_amount = int(order.total_amount) / 100.0,
check_no_accounts = order.total_accountnos,
no_transactions = order.nr_posts,
testcode = order.testcode,
file = base64.encodestring(clieopfile.rawdata),
filename = 'Clieop03-{0}.txt'.format(order.identification),
daynumber = int(clieopfile.header.file_id[2:]),
payment_order_ids = [
[6, 0, [x.id for x in clieop_export['payment_order_ids']]]
],
), context)
self.write(cr, uid, [ids[0]], dict(
filetype = order.name_transactioncode,
testcode = order.testcode,
file_id = file_id,
state = 'finish',
filetype=order.name_transactioncode,
identification=order.identification,
prefered_date=strfdate(order.preferred_execution_date),
total_amount=int(order.total_amount) / 100.0,
check_no_accounts=order.total_accountnos,
no_transactions=order.nr_posts,
testcode=order.testcode,
file=base64.encodestring(clieopfile.rawdata),
filename='Clieop03-{0}.txt'.format(order.identification),
daynumber=int(clieopfile.header.file_id[2:]),
payment_order_ids=[
[6, 0, [x.id
for x in clieop_export['payment_order_ids']]]],
), context)
self.write(cr, uid, [ids[0]], dict(
filetype=order.name_transactioncode,
testcode=order.testcode,
file_id=file_id,
state='finish',
), context)
return {
'name': _('Client Opdrachten Export'),
'view_type': 'form',
@@ -367,7 +377,9 @@ class banking_export_clieop_wizard(orm.TransientModel):
Cancel the ClieOp: just drop the file
'''
clieop_export = self.read(cr, uid, ids, ['file_id'], context)[0]
self.pool.get('banking.export.clieop').unlink(cr, uid, clieop_export['file_id'][0])
self.pool.get('banking.export.clieop').unlink(
cr, uid, clieop_export['file_id'][0]
)
return {'type': 'ir.actions.act_window_close'}
def save_clieop(self, cr, uid, ids, context):
@@ -378,11 +390,12 @@ class banking_export_clieop_wizard(orm.TransientModel):
cr, uid, ids, context)[0]
if not clieop_export['test']:
clieop_obj = self.pool.get('banking.export.clieop')
payment_order_obj = self.pool.get('payment.order')
clieop_file = clieop_obj.write(
clieop_obj.write(
cr, uid, clieop_export['file_id'].id, {'state': 'sent'}
)
wf_service = netsvc.LocalService('workflow')
for order in clieop_export['payment_order_ids']:
wf_service.trg_validate(uid, 'payment.order', order.id, 'sent', cr)
wf_service.trg_validate(
uid, 'payment.order', order.id, 'sent', cr
)
return {'type': 'ir.actions.act_window_close'}

View File

@@ -11,8 +11,8 @@
# garantees and support are strongly adviced to contract EduSense BV
#
# 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
# 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,
@@ -24,6 +24,5 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import girotel
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import girotel

View File

@@ -4,8 +4,8 @@
# 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
# 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,
@@ -26,7 +26,6 @@
'category': 'Account Banking',
'depends': ['account_banking'],
'data': [
#'security/ir.model.access.csv',
],
'description': '''
Module to import Dutch Girotel format transation files.

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -40,7 +40,8 @@ Assumptions:
4. the data comes from the SWIFT-network (limited ASCII)
Assumption 4 seems not always true, leading to wrong character conversions.
As a counter measure, all imported data is converted to SWIFT-format before usage.
As a counter measure, all imported data is converted to SWIFT-format before
usage.
'''
from account_banking.parsers import models
from account_banking.parsers.convert import str2date, to_swift
@@ -52,6 +53,7 @@ bt = models.mem_bank_transaction
__all__ = ['parser']
class transaction_message(object):
'''
A auxiliary class to validate and coerce read values
@@ -81,7 +83,12 @@ class transaction_message(object):
Convert values from string content to SWIFT-allowable content
'''
retval = super(transaction_message, self).__getattribute__(attr)
return attr != 'strattrs' and attr in self.strattrs and to_swift(retval) or retval
return attr != (
'strattrs'
and attr in self.strattrs
and to_swift(retval)
or retval
)
def genid(self):
'''
@@ -99,14 +106,15 @@ class transaction_message(object):
Initialize own dict with attributes and coerce values to right type
'''
if len(self.attrnames) != len(values):
raise ValueError, \
_('Invalid transaction line: expected %d columns, found %d') \
% (len(self.attrnames), len(values))
raise ValueError(
_('Invalid transaction line: expected %d columns, found %d')
% (len(self.attrnames), len(values))
)
self.__dict__.update(dict(zip(self.attrnames, values)))
self.date = str2date(self.date, '%Y%m%d')
if self.direction == 'A':
self.transferred_amount = -float(self.transferred_amount)
#payment batch done via clieop
# payment batch done via clieop
if (self.transfer_type == 'VZ'
and (not self.remote_account or self.remote_account == '0')
and (not self.message or re.match('^\s*$', self.message))
@@ -114,7 +122,7 @@ class transaction_message(object):
self.transfer_type = 'PB'
self.message = self.remote_owner
self.remove_owner = False
#payment batch done via sepa
# payment batch done via sepa
if self.transfer_type == 'VZ'\
and not self.remote_account\
and not self.remote_owner\
@@ -144,10 +152,10 @@ class transaction(models.mem_bank_transaction):
'''
Implementation of transaction communication class for account_banking.
'''
attrnames = [ 'statement_id', 'remote_account', 'remote_owner',
attrnames = ['statement_id', 'remote_account', 'remote_owner',
'remote_currency', 'transferred_amount', 'execution_date',
'value_date', 'transfer_type', 'message',
]
]
type_map = {
'BA': bt.PAYMENT_TERMINAL,
@@ -225,7 +233,7 @@ class transaction(models.mem_bank_transaction):
self.reference = self.remote_owner.rstrip()
parts = [self.message[i:i+32].rstrip()
for i in range(0, len(self.message), 32)
]
]
if len(parts) > 3:
self.reference = parts[-1]
self.message = '\n'.join(parts[:-1])
@@ -246,7 +254,7 @@ class transaction(models.mem_bank_transaction):
self.message = self.refold_message(self.message)
self.reference = '%s %s' % (self.remote_owner,
' '.join(self.message.split()[2:4])
)
)
elif self.transfer_type == 'IC':
# Direct debit - remote_owner containts reference, while
@@ -256,7 +264,7 @@ class transaction(models.mem_bank_transaction):
# taxes, but then a once-only manual correction is sufficient.
parts = [self.message[i:i+32].rstrip()
for i in range(0, len(self.message), 32)
]
]
self.reference = self.remote_owner
if not parts:
@@ -306,6 +314,7 @@ class transaction(models.mem_bank_transaction):
# message parts.
self.message = self.refold_message(self.message)
class statement(models.mem_bank_statement):
'''
Implementation of bank_statement communication class of account_banking
@@ -329,6 +338,7 @@ class statement(models.mem_bank_statement):
self.end_balance += trans.transferred_amount
self.transactions.append(trans)
class parser(models.parser):
code = 'NLGT'
name = _('Dutch Girotel - Kommagescheiden')

View File

@@ -3,9 +3,9 @@
# Copyright (C) 2011 Therp BV (<http://therp.nl>)
# (C) 2011 Smile BV (<http://smile.fr>)
#
# Based on account-banking
# Based on account-banking
# (C) 2009 - 2011 EduSense BV (<http://www.edusense.nl>)
#
#
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify

View File

@@ -24,10 +24,8 @@
#
##############################################################################
from datetime import datetime
from openerp.addons.account_banking.parsers import models
from openerp.addons.account_banking.parsers.convert import str2date
from openerp.addons.account_banking.sepa import postalcode
from openerp.tools.translate import _
import re
@@ -39,10 +37,12 @@ bt = models.mem_bank_transaction
"""
First line states the legend
"Datum","Naam / Omschrijving","Rekening","Tegenrekening","Code","Af Bij","Bedrag (EUR)","MutatieSoort","Mededelingen
"Datum","Naam / Omschrijving","Rekening","Tegenrekening","Code","Af Bij",\
"Bedrag (EUR)","MutatieSoort","Mededelingen
"""
class transaction_message(object):
'''
A auxiliary class to validate and coerce read values
@@ -68,16 +68,18 @@ class transaction_message(object):
if self.debcred == 'Af':
self.transferred_amount = -self.transferred_amount
try:
self.execution_date = self.value_date = str2date(self.date, '%Y%m%d')
self.execution_date = self.value_date = str2date(self.date,
'%Y%m%d')
except ValueError:
self.execution_date = self.value_date = str2date(self.date, '%d-%m-%Y')
self.statement_id = '' #self.value_date.strftime('%Yw%W')
self.execution_date = self.value_date = str2date(self.date,
'%d-%m-%Y')
self.statement_id = '' # self.value_date.strftime('%Yw%W')
self.id = str(subno).zfill(4)
self.reference = ''
# Normalize basic account numbers
self.remote_account = self.remote_account.replace('.', '').zfill(10)
self.local_account = self.local_account.replace('.', '').zfill(10)
self.local_account = self.local_account.replace('.', '').zfill(10)
class transaction(models.mem_bank_transaction):
'''
@@ -87,30 +89,30 @@ class transaction(models.mem_bank_transaction):
'remote_owner', 'transferred_amount',
'execution_date', 'value_date', 'transfer_type',
'id', 'reference', 'statement_id', 'message',
]
]
"""
Presumably the same transaction types occur in the MT940 format of ING.
From www.ing.nl/Images/MT940_Technische_handleiding_tcm7-69020.pdf
"""
type_map = {
'AC': bt.ORDER, # Acceptgiro
'BA': bt.PAYMENT_TERMINAL, # Betaalautomaattransactie
'CH': bt.ORDER, # Cheque
'DV': bt.ORDER, # Diversen
'FL': bt.BANK_TERMINAL, # Filiaalboeking, concernboeking
'GF': bt.ORDER, # Telefonisch bankieren
'GM': bt.BANK_TERMINAL, # Geldautomaat
'GT': bt.ORDER, # Internetbankieren
'IC': bt.DIRECT_DEBIT, # Incasso
'OV': bt.ORDER, # Overschrijving
'PK': bt.BANK_TERMINAL, # Opname kantoor
'PO': bt.ORDER, # Periodieke overschrijving
'ST': bt.BANK_TERMINAL, # Storting (eigen rekening of derde)
'VZ': bt.ORDER, # Verzamelbetaling
'NO': bt.STORNO, # Storno
'AC': bt.ORDER, # Acceptgiro
'BA': bt.PAYMENT_TERMINAL, # Betaalautomaattransactie
'CH': bt.ORDER, # Cheque
'DV': bt.ORDER, # Diversen
'FL': bt.BANK_TERMINAL, # Filiaalboeking, concernboeking
'GF': bt.ORDER, # Telefonisch bankieren
'GM': bt.BANK_TERMINAL, # Geldautomaat
'GT': bt.ORDER, # Internetbankieren
'IC': bt.DIRECT_DEBIT, # Incasso
'OV': bt.ORDER, # Overschrijving
'PK': bt.BANK_TERMINAL, # Opname kantoor
'PO': bt.ORDER, # Periodieke overschrijving
'ST': bt.BANK_TERMINAL, # Storting (eigen rekening of derde)
'VZ': bt.ORDER, # Verzamelbetaling
'NO': bt.STORNO, # Storno
}
# global expression for matching storno references
@@ -167,10 +169,6 @@ class transaction(models.mem_bank_transaction):
in the 'name' column, as well as full address information
in the 'message' column'
"""
reference = ''
street = False
zipcode = False
street = False
before = False
if self.remote_owner.startswith('KN: '):
self.reference = self.remote_owner[4:]
@@ -212,8 +210,7 @@ class transaction(models.mem_bank_transaction):
elif not self.execution_date:
self.error_message = "No execution date"
elif not self.remote_account and self.transfer_type not in [
'BA', 'FL', 'GM', 'IC', 'PK', 'ST'
]:
'BA', 'FL', 'GM', 'IC', 'PK', 'ST']:
self.error_message = (
"No remote account for transaction type %s" %
self.transfer_type)
@@ -225,6 +222,7 @@ class transaction(models.mem_bank_transaction):
No processing done here for Triodos, maybe later.
'''
class statement(models.mem_bank_statement):
'''
Implementation of bank_statement communication class of account_banking
@@ -240,7 +238,7 @@ class statement(models.mem_bank_statement):
self.date = str2date(msg.date, '%Y%m%d')
except ValueError:
self.date = str2date(msg.date, '%d-%m-%Y')
self.start_balance = self.end_balance = 0 # msg.start_balance
self.start_balance = self.end_balance = 0 # msg.start_balance
self.import_transaction(msg)
def import_transaction(self, msg):
@@ -251,6 +249,7 @@ class statement(models.mem_bank_statement):
self.end_balance += trans.transferred_amount
self.transactions.append(trans)
class parser(models.parser):
code = 'ING'
country_code = 'NL'
@@ -287,5 +286,3 @@ Statements.
stmnt = statement(msg)
result.append(stmnt)
return result
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -19,19 +19,19 @@
#
##############################################################################
{
"name" : "MT940 import for Dutch ING",
"version" : "1.1",
"author" : "Therp BV",
"name": "MT940 import for Dutch ING",
"version": "1.1",
"author": "Therp BV",
"complexity": "normal",
"description": """
This addon imports the structured MT940 format as offered by the Dutch ING
bank.
""",
"category" : "Account Banking",
"depends" : [
"category": "Account Banking",
"depends": [
'account_banking_mt940',
],
"data" : [
"data": [
],
"js": [
],
@@ -42,7 +42,7 @@ bank.
"auto_install": False,
"installable": True,
"application": False,
"external_dependencies" : {
'python' : [],
"external_dependencies": {
'python': [],
},
}

View File

@@ -20,8 +20,10 @@
##############################################################################
import re
from openerp.tools.translate import _
from openerp.addons.account_banking.parsers.models import parser,\
mem_bank_transaction
from openerp.addons.account_banking.parsers.models import (
parser,
mem_bank_transaction,
)
from openerp.addons.account_banking_mt940.mt940 import MT940, str2float
@@ -30,6 +32,7 @@ class transaction(mem_bank_transaction):
'''allow transactions without remote account'''
return bool(self.execution_date) and bool(self.transferred_amount)
class IngMT940Parser(MT940, parser):
name = _('ING MT940 (structured)')
country_code = 'NL'
@@ -104,5 +107,5 @@ class IngMT940Parser(MT940, parser):
if not subfields:
self.current_transaction.message = data
self.current_transaction = None

View File

@@ -11,8 +11,8 @@
# garantees and support are strongly adviced to contract EduSense BV
#
# 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
# 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,
@@ -24,6 +24,5 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import multibank
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import multibank

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -37,6 +37,7 @@ __all__ = ['parser']
bt = models.mem_bank_transaction
class transaction_message(object):
'''
A auxiliary class to validate and coerce read values
@@ -60,7 +61,7 @@ class transaction_message(object):
Banking Tools regulations, it is considered to be used by all banks
in the Netherlands which comply to it. If not, please notify us.
'''
if len(accountno) == 10: # Invalid: longest number is 9
if len(accountno) == 10: # Invalid: longest number is 9
accountno = accountno[1:]
# 9-scheme or 7-scheme?
stripped = accountno.lstrip('0')
@@ -73,18 +74,18 @@ class transaction_message(object):
Initialize own dict with attributes and coerce values to right type
'''
if len(self.attrnames) != len(values):
raise ValueError, \
_('Invalid transaction line: expected %d columns, found %d') \
% (len(self.attrnames), len(values))
raise ValueError(
_('Invalid transaction line: expected %d columns, found %d')
% (len(self.attrnames), len(values))
)
self.__dict__.update(dict(zip(self.attrnames, values)))
#self.local_account = self.clean_account(self.local_account)
#self.remote_account = self.clean_account(self.remote_account)
self.start_balance = float(self.start_balance)
self.transferred_amount = float(self.transferred_amount)
self.execution_date = str2date(self.execution_date, '%d-%m-%Y')
self.value_date = str2date(self.value_date, '%d-%m-%Y')
self.id = str(subno).zfill(4)
class transaction(models.mem_bank_transaction):
'''
Implementation of transaction communication class for account_banking.
@@ -93,7 +94,7 @@ class transaction(models.mem_bank_transaction):
'remote_owner', 'remote_currency', 'transferred_amount',
'execution_date', 'value_date', 'transfer_type',
'reference', 'message', 'statement_id', 'id',
]
]
type_map = {
'ACC': bt.ORDER,
@@ -151,14 +152,12 @@ class transaction(models.mem_bank_transaction):
two accounts - the cash exits the banking system. These withdrawals
have their transfer_type set to 'OPN'.
'''
return (self.transferred_amount and self.execution_date and
self.value_date) and (
self.remote_account or
self.transfer_type in [
'KST', 'PRV', 'BTL', 'BEA', 'OPN', 'KNT', 'DIV',
]
and not self.error_message
)
return ((
self.transferred_amount and self.execution_date
and self.value_date)
and (self.remote_account or self.transfer_type in [
'KST', 'PRV', 'BTL', 'BEA', 'OPN', 'KNT', 'DIV'
] and not self.error_message))
def parse_message(self):
'''
@@ -172,7 +171,7 @@ class transaction(models.mem_bank_transaction):
elif self.transfer_type == 'BEA':
# Payment through payment terminal
# Remote owner is part of message, while remote_owner is set
# Remote owner is part of message, while remote_owner is set
# to the intermediate party, which we don't need.
self.remote_owner = self.message[:23].rstrip()
self.remote_owner_city = self.message[23:31].rstrip()
@@ -188,8 +187,10 @@ class transaction(models.mem_bank_transaction):
# The ordered transferred amount
currency, amount = part.split('. ')[1].split()
if self.remote_currency != currency.upper():
self.error_message = \
'Remote currency in message differs from transaction.'
self.error_message = (
'Remote currency in message differs from '
'transaction.'
)
else:
self.local_amount = float(amount)
elif part.startswith('koers. '):
@@ -262,6 +263,7 @@ class transaction(models.mem_bank_transaction):
parts = parts[:-1]
self.message = ' '.join(parts)
class statement(models.mem_bank_statement):
'''
Implementation of bank_statement communication class of account_banking
@@ -285,6 +287,7 @@ class statement(models.mem_bank_statement):
self.end_balance += trans.transferred_amount
self.transactions.append(trans)
class parser(models.parser):
code = 'NLBT'
country_code = 'NL'

View File

@@ -13,8 +13,8 @@
# or Therp BV
#
# 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
# 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,
@@ -26,6 +26,5 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import triodos
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import triodos

View File

@@ -5,8 +5,8 @@
# 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
# 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,

View File

@@ -6,8 +6,8 @@
# 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
# 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,
@@ -28,10 +28,9 @@ Dutch Banking Tools uses the concept of 'Afschrift' or Bank Statement.
Every transaction is bound to a Bank Statement. As such, this module generates
Bank Statements along with Bank Transactions.
'''
from datetime import datetime
from account_banking.parsers import models
from account_banking.parsers.convert import str2date
from account_banking.sepa import postalcode
from tools.translate import _
import re
@@ -41,6 +40,7 @@ __all__ = ['parser']
bt = models.mem_bank_transaction
class transaction_message(object):
'''
A auxiliary class to validate and coerce read values
@@ -72,50 +72,57 @@ class transaction_message(object):
self.remote_account = self.remote_account.replace('.', '').zfill(10)
self.local_account = self.local_account.replace('.', '').zfill(10)
class transaction(models.mem_bank_transaction):
'''
Implementation of transaction communication class for account_banking.
'''
attrnames = ['local_account', 'remote_account',
'remote_owner', 'transferred_amount',
'execution_date', 'value_date', 'transfer_type',
'reference', 'id',
]
attrnames = [
'local_account',
'remote_account',
'remote_owner',
'transferred_amount',
'execution_date',
'value_date',
'transfer_type',
'reference',
'id',
]
type_map = {
# retrieved from online help in the Triodos banking application
'AC': bt.ORDER, # Acceptgiro gecodeerd
'AN': bt.ORDER, # Acceptgiro ongecodeerd
'AT': bt.ORDER, # Acceptgiro via internet
'BA': bt.PAYMENT_TERMINAL, # Betaalautomaat
'CHIP': bt.BANK_TERMINAL, # Chipknip
# 'CO': # Correctie
'DB': bt.ORDER, # Diskettebetaling
# 'DV': # Dividend
'EI': bt.DIRECT_DEBIT, # Europese Incasso
'EICO': bt.DIRECT_DEBIT, # Europese Incasso Correctie
'EIST': bt.ORDER, # Europese Incasso Storno
'ET': bt.ORDER, # Europese Transactie
'ETST': bt.ORDER, #Europese Transactie Storno
'GA': bt.BANK_TERMINAL, # Geldautomaat
'IB': bt.ORDER, # Interne Boeking
'IC': bt.DIRECT_DEBIT, # Incasso
'ID': bt.ORDER, # iDeal-betaling
'IT': bt.ORDER, # Internet transactie
'KN': bt.BANK_COSTS, # Kosten
'KO': bt.BANK_TERMINAL, # Kasopname
# 'KS': # Kwaliteitsstoring
'OV': bt.ORDER, # Overboeking. NB: can also be bt.BANK_COSTS
# when no remote_account specified!
'PO': bt.ORDER, # Periodieke Overboeking
'PR': bt.BANK_COSTS, # Provisie
# 'RE': # Rente
# 'RS': # Renteschenking
'ST': bt.ORDER, # Storno
'TG': bt.ORDER, # Telegiro
# 'VL': # Vaste Lening
'VO': bt.DIRECT_DEBIT, # Vordering overheid
'VV': bt.ORDER, # Vreemde valuta
'AC': bt.ORDER, # Acceptgiro gecodeerd
'AN': bt.ORDER, # Acceptgiro ongecodeerd
'AT': bt.ORDER, # Acceptgiro via internet
'BA': bt.PAYMENT_TERMINAL, # Betaalautomaat
'CHIP': bt.BANK_TERMINAL, # Chipknip
# 'CO': # Correctie
'DB': bt.ORDER, # Diskettebetaling
# 'DV': # Dividend
'EI': bt.DIRECT_DEBIT, # Europese Incasso
'EICO': bt.DIRECT_DEBIT, # Europese Incasso Correctie
'EIST': bt.ORDER, # Europese Incasso Storno
'ET': bt.ORDER, # Europese Transactie
'ETST': bt.ORDER, # Europese Transactie Storno
'GA': bt.BANK_TERMINAL, # Geldautomaat
'IB': bt.ORDER, # Interne Boeking
'IC': bt.DIRECT_DEBIT, # Incasso
'ID': bt.ORDER, # iDeal-betaling
'IT': bt.ORDER, # Internet transactie
'KN': bt.BANK_COSTS, # Kosten
'KO': bt.BANK_TERMINAL, # Kasopname
# 'KS': # Kwaliteitsstoring
'OV': bt.ORDER, # Overboeking. NB: can also be bt.BANK_COSTS
# when no remote_account specified!
'PO': bt.ORDER, # Periodieke Overboeking
'PR': bt.BANK_COSTS, # Provisie
# 'RE': # Rente
# 'RS': # Renteschenking
'ST': bt.ORDER, # Storno
'TG': bt.ORDER, # Telegiro
# 'VL': # Vaste Lening
'VO': bt.DIRECT_DEBIT, # Vordering overheid
'VV': bt.ORDER, # Vreemde valuta
}
def __init__(self, line, *args, **kwargs):
@@ -129,9 +136,9 @@ class transaction(models.mem_bank_transaction):
self.message = ''
# Decompose structured messages
self.parse_message()
if (self.transfer_type == 'OV' and
not self.remote_account and
not self.remote_owner):
if (self.transfer_type == 'OV'
and not self.remote_account
and not self.remote_owner):
self.transfer_type = 'KN'
def is_valid(self):
@@ -141,8 +148,7 @@ class transaction(models.mem_bank_transaction):
elif not self.execution_date:
self.error_message = "No execution date"
elif not self.remote_account and self.transfer_type not in [
'KN', 'TG', 'GA', 'BA', 'CHIP'
]:
'KN', 'TG', 'GA', 'BA', 'CHIP']:
self.error_message = (
"No remote account for transaction type %s" %
self.transfer_type)
@@ -172,7 +178,7 @@ class statement(models.mem_bank_statement):
self.id = msg.statement_id
self.local_account = msg.local_account
self.date = str2date(msg.date, '%d-%m-%Y')
self.start_balance = self.end_balance = 0 # msg.start_balance
self.start_balance = self.end_balance = 0 # msg.start_balance
self.import_transaction(msg)
def import_transaction(self, msg):
@@ -183,6 +189,7 @@ class statement(models.mem_bank_statement):
self.end_balance += trans.transferred_amount
self.transactions.append(trans)
class parser(models.parser):
code = 'TRIOD'
country_code = 'NL'
@@ -220,5 +227,3 @@ Statements.
stmnt = statement(msg)
result.append(stmnt)
return result
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -40,11 +40,16 @@
Base module for PAIN file generation
====================================
This module contains fields and functions that are used by the module for SEPA Credit Transfer (account_banking_sepa_credit_transfer) and SEPA Direct Debit (account_banking_sepa_direct_debit). This module doesn't provide any functionnality by itself.
This module contains fields and functions that are used by the module for SEPA
Credit Transfer (account_banking_sepa_credit_transfer) and SEPA Direct Debit
(account_banking_sepa_direct_debit). This module doesn't provide any
functionnality by itself.
This module is part of the banking addons: https://launchpad.net/banking-addons
This module is part of the banking addons:
https://www.github.com/OCA/banking-addons
This module was started during the Akretion-Noviat code sprint of November 21st 2013 in Epiais les Louvres (France).
This module was started during the Akretion-Noviat code sprint of
November 21st 2013 in Epiais les Louvres (France).
''',
'active': False,
'installable': True,

View File

@@ -50,7 +50,7 @@ class res_company(orm.Model):
if country_code == 'BE':
party_identifier = company_vat[2:].replace(' ', '')
elif country_code == 'ES':
party_identifier = company.sepa_creditor_identifier
party_identifier = company.sepa_creditor_identifier
return party_identifier
def _initiating_party_issuer_default(self, cr, uid, context=None):

View File

@@ -4,8 +4,8 @@
# 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
# 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,
@@ -18,7 +18,8 @@
#
##############################################################################
{
'name': 'Banking Addons - Default partner journal accounts for bank transactions',
'name': 'Banking Addons - Default partner journal accounts for bank'
' transactions',
'version': '0.1',
'license': 'AGPL-3',
'author': 'Therp BV',

View File

@@ -63,6 +63,7 @@ class ResPartner(orm.Model):
res = super(ResPartner, self).def_journal_account_bank_incr(
cr, uid, ids, context=context)
for partner in self.browse(cr, uid, ids, context=context):
if partner.property_account_receivable_bank_id:
res[partner.id] = partner.property_account_receivable_bank_id.id
bank = partner.property_account_receivable_bank_id
if bank:
res[partner.id] = bank.id
return res

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -41,7 +41,7 @@
'workflow/account_payment.xml',
],
'description': '''
This addon adds payment reconciliation infrastructure to the Banking Addons.
This addon adds payment reconciliation infrastructure to the Banking Addons.
* Extends payments for digital banking:
+ Adapted workflow in payments to reflect banking operations

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -32,14 +32,12 @@ class banking_import_line(orm.TransientModel):
'payment_order_id': fields.many2one(
'payment.order', 'Payment order'),
'transaction_type': fields.selection([
# Add payment order related transaction types
('invoice', 'Invoice payment'),
('payment_order_line', 'Payment from a payment order'),
('payment_order', 'Aggregate payment order'),
('storno', 'Canceled debit order'),
('bank_costs', 'Bank costs'),
('unknown', 'Unknown'),
], 'Transaction type'),
}
# Add payment order related transaction types
('invoice', 'Invoice payment'),
('payment_order_line', 'Payment from a payment order'),
('payment_order', 'Aggregate payment order'),
('storno', 'Canceled debit order'),
('bank_costs', 'Bank costs'),
('unknown', 'Unknown'),
], 'Transaction type'),
}

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -27,14 +27,16 @@ from openerp.osv import orm, fields
from openerp import netsvc
from openerp.tools.translate import _
from openerp.addons.decimal_precision import decimal_precision as dp
from openerp.addons.account_banking.parsers.models import mem_bank_transaction as bt
from openerp.addons.account_banking.parsers.models import (
mem_bank_transaction as bt
)
class banking_import_transaction(orm.Model):
_inherit = 'banking.import.transaction'
def _match_payment_order(
self, cr, uid, trans, log, order_type='payment', context=None):
self, cr, uid, trans, log, order_type='payment', context=None):
def equals_order_amount(payment_order, transferred_amount):
if (not hasattr(payment_order, 'payment_order_type')
@@ -60,25 +62,26 @@ class banking_import_transaction(orm.Model):
if len(candidates) > 0:
# retrieve the common account_id, if any
account_id = False
if (candidates[0].line_ids[0].transit_move_line_id):
for line in candidates[0].line_ids[0].transit_move_line_id.move_id.line_id:
transit_move_lines = candidates[0].line_ids[0].transit_move_line_id
if transit_move_lines:
for line in transit_move_lines.move_id.line_id:
if line.account_id.type == 'other':
account_id = line.account_id.id
break
return dict(
move_line_ids = False,
match_type = 'payment_order',
payment_order_ids = [x.id for x in candidates],
account_id = account_id,
partner_id = False,
partner_bank_id = False,
reference = False,
move_line_ids=False,
match_type='payment_order',
payment_order_ids=[x.id for x in candidates],
account_id=account_id,
partner_id=False,
partner_bank_id=False,
reference=False,
type='general',
)
)
return False
def _match_storno(
self, cr, uid, trans, log, context=None):
self, cr, uid, trans, log, context=None):
payment_line_obj = self.pool.get('payment.line')
line_ids = payment_line_obj.search(
cr, uid, [
@@ -93,21 +96,21 @@ class banking_import_transaction(orm.Model):
trans.statement_id.currency, context=None)
if account_id:
return dict(
account_id = account_id,
match_type = 'storno',
payment_line_id = line_ids[0],
account_id=account_id,
match_type='storno',
payment_line_id=line_ids[0],
move_line_ids=False,
partner_id=False,
partner_bank_id=False,
reference=False,
type='customer',
)
)
# TODO log the reason why there is no result for transfers marked
# as storno
return False
def _match_payment(self, cr, uid, trans, payment_lines,
partner_ids, bank_account_ids, log, linked_payments):
partner_ids, bank_account_ids, log, linked_payments):
'''
Find the payment order belonging to this reference - if there is one
This is the easiest part: when sending payments, the returned bank info
@@ -132,7 +135,7 @@ class banking_import_transaction(orm.Model):
digits = dp.get_precision('Account')(cr)[1]
candidates = [
line for line in payment_lines
if (line.communication == trans.reference
if (line.communication == trans.reference
and round(line.amount, digits) == -round(
trans.statement_line_id.amount, digits)
and bank_match(trans.remote_account, line.bank_id))
@@ -145,15 +148,15 @@ class banking_import_transaction(orm.Model):
move_info = self._get_move_info(
cr, uid, [candidate.move_line_id.id])
move_info.update({
'match_type': 'payment',
'payment_line_id': candidate.id,
})
'match_type': 'payment',
'payment_line_id': candidate.id,
})
return move_info
return False
def _confirm_storno(
self, cr, uid, transaction_id, context=None):
self, cr, uid, transaction_id, context=None):
"""
Creation of the reconciliation has been delegated to
*a* direct debit module, to allow for various direct debit styles
@@ -167,18 +170,18 @@ class banking_import_transaction(orm.Model):
_("No direct debit order item"))
reconcile_id = payment_line_pool.debit_storno(
cr, uid,
transaction.payment_line_id.id,
transaction.payment_line_id.id,
transaction.statement_line_id.amount,
transaction.statement_line_id.currency,
transaction.storno_retry,
context=context)
statement_line_pool.write(
cr, uid, transaction.statement_line_id.id,
cr, uid, transaction.statement_line_id.id,
{'reconcile_id': reconcile_id}, context=context)
transaction.refresh()
def _confirm_payment_order(
self, cr, uid, transaction_id, context=None):
self, cr, uid, transaction_id, context=None):
"""
Creation of the reconciliation has been delegated to
*a* direct debit module, to allow for various direct debit styles
@@ -197,11 +200,11 @@ class banking_import_transaction(orm.Model):
transaction.statement_line_id.currency,
context=context)
statement_line_pool.write(
cr, uid, transaction.statement_line_id.id,
cr, uid, transaction.statement_line_id.id,
{'reconcile_id': reconcile_id}, context=context)
def _confirm_payment(
self, cr, uid, transaction_id, context=None):
self, cr, uid, transaction_id, context=None):
"""
Do some housekeeping on the payment line
then pass on to _reconcile_move
@@ -227,7 +230,7 @@ class banking_import_transaction(orm.Model):
uid, 'payment.order', order_id, 'done', cr)
def _cancel_payment(
self, cr, uid, transaction_id, context=None):
self, cr, uid, transaction_id, context=None):
"""
Do not support cancelling individual lines yet, because the workflow
of the payment order does not support reopening.
@@ -238,7 +241,7 @@ class banking_import_transaction(orm.Model):
"match type 'payment'"))
def _cancel_payment_order(
self, cr, uid, transaction_id, context=None):
self, cr, uid, transaction_id, context=None):
"""
"""
payment_order_obj = self.pool.get('payment.order')
@@ -259,7 +262,7 @@ class banking_import_transaction(orm.Model):
transaction.statement_line_id.currency)
def _cancel_storno(
self, cr, uid, transaction_id, context=None):
self, cr, uid, transaction_id, context=None):
"""
TODO: delegate unreconciliation to the direct debit module,
to allow for various direct debit styles
@@ -267,7 +270,7 @@ class banking_import_transaction(orm.Model):
payment_line_obj = self.pool.get('payment.line')
reconcile_obj = self.pool.get('account.move.reconcile')
transaction = self.browse(cr, uid, transaction_id, context=context)
if not transaction.payment_line_id:
raise orm.except_orm(
_("Cannot cancel link with storno"),
@@ -295,7 +298,8 @@ class banking_import_transaction(orm.Model):
raise orm.except_orm(
_("Cannot cancel link with storno"),
_("Line id not found"))
reconcile = cancel_line.reconcile_id or cancel_line.reconcile_partial_id
reconcile = (cancel_line.reconcile_id
or cancel_line.reconcile_partial_id)
lines_reconcile = reconcile.line_id or reconcile.line_partial_ids
if len(lines_reconcile) < 3:
# delete the full reconciliation
@@ -303,15 +307,15 @@ class banking_import_transaction(orm.Model):
else:
# we are left with a partial reconciliation
reconcile_obj.write(
cr, uid, reconcile.id,
{'line_partial_ids':
cr, uid, reconcile.id,
{'line_partial_ids':
[(6, 0, [x.id for x in lines_reconcile
if x.id != cancel_line.id])],
'line_id': [(6, 0, [])],
}, context)
# redo the original payment line reconciliation with the invoice
payment_line_obj.write(
cr, uid, transaction.payment_line_id.id,
cr, uid, transaction.payment_line_id.id,
{'storno': False}, context)
payment_line_obj.debit_reconcile(
cr, uid, transaction.payment_line_id.id, context)
@@ -333,7 +337,7 @@ class banking_import_transaction(orm.Model):
for transaction in self.browse(cr, uid, ids, context):
if transaction.match_type == 'payment_order':
if (transaction.payment_order_ids and not
transaction.payment_order_id):
transaction.payment_order_id):
res[transaction.id] = True
return res
@@ -354,7 +358,7 @@ class banking_import_transaction(orm.Model):
vals['payment_order_ids'] = [
(6, 0, move_info.get('payment_order_ids') or [])]
vals['payment_order_id'] = (
move_info.get('payment_order_ids', False) and
move_info.get('payment_order_ids', False) and
len(move_info['payment_order_ids']) == 1 and
move_info['payment_order_ids'][0]
)
@@ -388,17 +392,19 @@ class banking_import_transaction(orm.Model):
super(banking_import_transaction, self).__init__(pool, cr)
self.confirm_map.update({
'storno': banking_import_transaction._confirm_storno,
'payment_order': banking_import_transaction._confirm_payment_order,
'payment': banking_import_transaction._confirm_payment,
'payment_order_manual': banking_import_transaction._confirm_payment_order,
'payment_manual': banking_import_transaction._confirm_payment,
})
'storno': banking_import_transaction._confirm_storno,
'payment_order': banking_import_transaction._confirm_payment_order,
'payment': banking_import_transaction._confirm_payment,
'payment_order_manual': (
banking_import_transaction._confirm_payment_order),
'payment_manual': banking_import_transaction._confirm_payment,
})
self.cancel_map.update({
'storno': banking_import_transaction._cancel_storno,
'payment_order': banking_import_transaction._cancel_payment_order,
'payment': banking_import_transaction._cancel_payment,
'payment_order_manual': banking_import_transaction._cancel_payment_order,
'payment_manual': banking_import_transaction._cancel_payment,
})
'storno': banking_import_transaction._cancel_storno,
'payment_order': banking_import_transaction._cancel_payment_order,
'payment': banking_import_transaction._cancel_payment,
'payment_order_manual': (
banking_import_transaction._cancel_payment_order),
'payment_manual': banking_import_transaction._cancel_payment,
})

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -53,19 +53,23 @@ class banking_transaction_wizard(orm.TransientModel):
sign = 1
else:
sign = -1
total = (payment_order.total + sign *
total = (payment_order.total + sign *
transaction_id.statement_line_id.amount)
if not self.pool.get('res.currency').is_zero(
cr, uid, transaction_id.statement_line_id.statement_id.currency, total):
cr, uid,
transaction_id.statement_line_id.statement_id.currency,
total):
raise orm.except_orm(
_('Error'),
_('When matching a payment order, the amounts have to '
'match exactly'))
if payment_order.mode and payment_order.mode.transfer_account_id:
if (payment_order.mode
and payment_order.mode.transfer_account_id):
transaction_id.statement_line_id.write({
'account_id': payment_order.mode.transfer_account_id.id,
})
'account_id': (
payment_order.mode.transfer_account_id.id),
})
write_vals.update(
{'payment_order_id': manual_payment_order_id,
'match_type': 'payment_order_manual'})
@@ -79,25 +83,40 @@ class banking_transaction_wizard(orm.TransientModel):
_columns = {
'payment_line_id': fields.related(
'import_transaction_id', 'payment_line_id',
string="Matching payment or storno",
type='many2one', relation='payment.line',
readonly=True),
'import_transaction_id',
'payment_line_id',
string="Matching payment or storno",
type='many2one',
relation='payment.line',
readonly=True,
),
'payment_order_ids': fields.related(
'import_transaction_id', 'payment_order_ids',
string="Matching payment orders",
type='many2many', relation='payment.order'),
'import_transaction_id',
'payment_order_ids',
string="Matching payment orders",
type='many2many',
relation='payment.order',
),
'payment_order_id': fields.related(
'import_transaction_id', 'payment_order_id',
string="Payment order to reconcile",
type='many2one', relation='payment.order'),
'import_transaction_id',
'payment_order_id',
string="Payment order to reconcile",
type='many2one',
relation='payment.order',
),
'manual_payment_order_id': fields.many2one(
'payment.order', 'Match this payment order',
domain=[('state', '=', 'sent')]),
'payment.order',
'Match this payment order',
domain=[
('state', '=', 'sent'),
],
),
'manual_payment_line_id': fields.many2one(
'payment.line', 'Match this payment line',
'payment.line',
'Match this payment line',
domain=[
('order_id.state', '=', 'sent'),
('date_done', '=', False),
]),
}
],
),
}

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -27,6 +27,7 @@ from openerp.osv import orm, fields
from openerp import netsvc
from openerp.tools.translate import _
class payment_line(orm.Model):
'''
Add some fields; make destination bank account
@@ -40,7 +41,7 @@ class payment_line(orm.Model):
'date_done': fields.date(
'Date Confirmed', select=True, readonly=True),
'transit_move_line_id': fields.many2one(
# this line is part of the credit side of move 2a
# this line is part of the credit side of move 2a
# from the documentation
'account.move.line', 'Debit move line',
readonly=True,
@@ -57,7 +58,7 @@ class payment_line(orm.Model):
account_direct_debit module.
"""
def get_storno_account_id(self, cr, uid, payment_line_id, amount,
currency_id, context=None):
currency_id, context=None):
"""
Hook for verifying a match of the payment line with the amount.
Return the account associated with the storno.
@@ -115,19 +116,21 @@ class payment_line(orm.Model):
if (not transit_move_line or not torec_move_line):
raise orm.except_orm(
_('Can not reconcile'),
_('No move line for line %s') % payment_line.name)
if torec_move_line.reconcile_id: # torec_move_line.reconcile_partial_id:
_('No move line for line %s') % payment_line.name
)
if torec_move_line.reconcile_id:
raise orm.except_orm(
_('Error'),
_('Move line %s has already been reconciled') %
_('Move line %s has already been reconciled') %
torec_move_line.name
)
if transit_move_line.reconcile_id or transit_move_line.reconcile_partial_id:
if (transit_move_line.reconcile_id
or transit_move_line.reconcile_partial_id):
raise orm.except_orm(
_('Error'),
_('Move line %s has already been reconciled') %
_('Move line %s has already been reconciled') %
transit_move_line.name
)
)
def is_zero(total):
return self.pool.get('res.currency').is_zero(
@@ -136,7 +139,7 @@ class payment_line(orm.Model):
line_ids = [transit_move_line.id, torec_move_line.id]
if torec_move_line.reconcile_partial_id:
line_ids = [
x.id for x in
x.id for x in
torec_move_line.reconcile_partial_id.line_partial_ids
] + [transit_move_line.id]
@@ -144,7 +147,9 @@ class payment_line(orm.Model):
vals = {
'type': 'auto',
'line_id': is_zero(total) and [(6, 0, line_ids)] or [(6, 0, [])],
'line_partial_ids': is_zero(total) and [(6, 0, [])] or [(6, 0, line_ids)],
'line_partial_ids': (is_zero(total)
and [(6, 0, [])]
or [(6, 0, line_ids)]),
}
if torec_move_line.reconcile_partial_id:

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -44,7 +44,7 @@ class payment_mode(orm.Model):
'a debit order of this mode'),
),
'payment_term_ids': fields.many2many(
'account.payment.term', 'account_payment_order_terms_rel',
'account.payment.term', 'account_payment_order_terms_rel',
'mode_id', 'term_id', 'Payment terms',
help=('Limit selected invoices to invoices with these payment '
'terms')

View File

@@ -4,7 +4,7 @@
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
# (C) 2014 ACSONE SA/NV (<http://acsone.eu>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved

View File

@@ -1 +1 @@
from . import model
from . import model

View File

@@ -56,8 +56,8 @@
],
'demo': ['demo/banking_demo.xml'],
'description': '''
Infrastructure to export payment orders
plus some bug fixes and obvious enhancements to payment orders
Infrastructure to export payment orders
plus some bug fixes and obvious enhancements to payment orders
that will hopefully land in offical addons one day.
This technical module provides the base infrastructure to export
@@ -65,21 +65,25 @@
technical features:
* a new payment.mode.type model
* payment.mode now has a mandatory type
* a better implementation of payment_mode.suitable_bank_types() based on payment.mode.type
* the "make payment" button launches a wizard depending on the payment.mode.type
* a manual payment mode type is provided as an example, with a default "do nothing" wizard
* a better implementation of payment_mode.suitable_bank_types() based
on payment.mode.type
* the "make payment" button launches a wizard depending on the
payment.mode.type
* a manual payment mode type is provided as an example, with a default
"do nothing" wizard
To enable the use of payment order to collect money for customers,
it adds a payment_order_type (payment|debit) as a basis of direct debit support
(this field becomes visible when account_direct_debit is installed).
Refactoring note: this field should ideally go in account_direct_debit,
but account_banking_payment currently depends on it.
To enable the use of payment order to collect money for customers,
it adds a payment_order_type (payment|debit) as a basis of direct debit
support (this field becomes visible when account_direct_debit is
installed).
Refactoring note: this field should ideally go in account_direct_debit,
but account_banking_payment currently depends on it.
Bug fixes and enhancement that should land in official addons:
* make the search function of the payment export wizard extensible
* fix lp:1275478: allow payment of customer refunds
* display the maturity date of the move lines when you are in
* display the maturity date of the move lines when you are in
the wizard to select the lines to pay
''',
''',
'installable': True,
}

View File

@@ -73,15 +73,18 @@ class account_move_line(orm.Model):
) %(operator)s %%s ''' % {'operator': x[1]}, args))
sql_args = tuple(map(itemgetter(2), args))
cr.execute(('''SELECT id
cr.execute(
('''\
SELECT id
FROM account_move_line l
WHERE account_id IN (select id
FROM account_account
WHERE type in %s AND active)
AND reconcile_id IS null
AND credit > 0
AND ''' + where + ' and ' + query),
(('payable', 'receivable'),)+sql_args )
AND ''' + where + ' and ' + query
), (('payable', 'receivable'), ) + sql_args
)
# The patch we have compared to the original function in
# addons/account_payment is just above :
# original code : type = 'payable'
@@ -93,6 +96,10 @@ class account_move_line(orm.Model):
return [('id', 'in', map(lambda x:x[0], res))]
_columns = {
'amount_to_pay': fields.function(amount_to_pay,
type='float', string='Amount to pay', fnct_search=_to_pay_search),
'amount_to_pay': fields.function(
amount_to_pay,
type='float',
string='Amount to pay',
fnct_search=_to_pay_search
),
}

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -52,7 +52,7 @@ class payment_order(orm.Model):
If type is manual. just confirm the order.
Previously (pre-v6) in account_payment/wizard/wizard_pay.py
"""
if context == None:
if context is None:
context = {}
result = {}
orders = self.browse(cr, uid, ids, context)
@@ -81,10 +81,13 @@ class payment_order(orm.Model):
if order.mode.type and order.mode.type.ir_model_id:
raise orm.except_orm(
_('Error'),
_('You can only combine payment orders of the same type')
)
_('You can only combine payment orders of the same '
'type')
)
# process manual payments
wf_service = netsvc.LocalService('workflow')
for order_id in ids:
wf_service.trg_validate(uid, 'payment.order', order_id, 'done', cr)
wf_service.trg_validate(
uid, 'payment.order', order_id, 'done', cr
)
return result

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -37,18 +37,24 @@ class payment_manual(orm.TransientModel):
_description = 'Send payment order(s) manually'
_columns = {
'payment_order_ids': fields.many2many('payment.order',
'wiz_manual_payorders_rel', 'wizard_id', 'payment_order_id',
'Payment orders', readonly=True),
}
'payment_order_ids': fields.many2many(
'payment.order',
'wiz_manual_payorders_rel',
'wizard_id',
'payment_order_id',
'Payment orders',
readonly=True
),
}
def create(self, cr, uid, vals, context=None):
payment_order_ids = context.get('active_ids', [])
vals.update({
'payment_order_ids': [[6, 0, payment_order_ids]],
})
return super(payment_manual, self).create(cr, uid,
vals, context=context)
return super(payment_manual, self).create(
cr, uid, vals, context=context
)
def button_ok(self, cr, uid, ids, context=None):
wf_service = netsvc.LocalService('workflow')

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -38,8 +38,9 @@ class payment_mode(orm.Model):
res = []
payment_mode = self.browse(
cr, uid, payment_mode_id, context)
if (payment_mode and payment_mode.type and
payment_mode.type.suitable_bank_types):
if (payment_mode
and payment_mode.type
and payment_mode.type.suitable_bank_types):
res = [t.code for t in payment_mode.type.suitable_bank_types]
return res

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -63,9 +63,11 @@ class payment_mode_type(orm.Model):
def _auto_init(self, cr, context=None):
r = super(payment_mode_type, self)._auto_init(cr, context=context)
# migrate xmlid from manual_bank_transfer to avoid dependency on account_banking
cr.execute("""UPDATE ir_model_data SET module='account_banking_payment_export'
WHERE module='account_banking' AND
name='manual_bank_tranfer' AND
# migrate xmlid from manual_bank_transfer to avoid dependency on
# account_banking
cr.execute("""UPDATE ir_model_data
SET module='account_banking_payment_export'
WHERE module='account_banking' AND
name='manual_bank_tranfer' AND
model='payment.mode.type'""")
return r

View File

@@ -3,7 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -50,8 +50,8 @@ class payment_order_create(orm.TransientModel):
context = {}
data = self.read(cr, uid, ids, ['duedate'], context=context)[0]
search_due_date = data['duedate']
### start account_banking_payment ###
# start account_banking_payment
payment = self.pool.get('payment.order').browse(
cr, uid, context['active_id'], context=context)
# Search for move line to pay:
@@ -62,7 +62,7 @@ class payment_order_create(orm.TransientModel):
]
self.extend_payment_order_domain(
cr, uid, payment, domain, context=context)
### end account_direct_debit ###
# end account_direct_debit
domain = domain + [
'|', ('date_maturity', '<=', search_due_date),
@@ -71,21 +71,22 @@ class payment_order_create(orm.TransientModel):
line_ids = line_obj.search(cr, uid, domain, context=context)
context.update({'line_ids': line_ids})
model_data_ids = mod_obj.search(
cr, uid,[
cr, uid, [
('model', '=', 'ir.ui.view'),
('name', '=', 'view_create_payment_order_lines')],
context=context)
resource_id = mod_obj.read(
cr, uid, model_data_ids, fields=['res_id'],
context=context)[0]['res_id']
return {'name': _('Entry Lines'),
'context': context,
'view_type': 'form',
'view_mode': 'form',
'res_model': 'payment.order.create',
'views': [(resource_id, 'form')],
'type': 'ir.actions.act_window',
'target': 'new',
return {
'name': _('Entry Lines'),
'context': context,
'view_type': 'form',
'view_mode': 'form',
'res_model': 'payment.order.create',
'views': [(resource_id, 'form')],
'type': 'ir.actions.act_window',
'target': 'new',
}
def _prepare_payment_line(self, cr, uid, payment, line, context=None):
@@ -93,26 +94,26 @@ class payment_order_create(orm.TransientModel):
The resulting dict is passed to the create method of payment.line'''
_today = fields.date.context_today(self, cr, uid, context=context)
if payment.date_prefered == "now":
#no payment date => immediate payment
# no payment date => immediate payment
date_to_pay = False
elif payment.date_prefered == 'due':
### account_banking
# account_banking
# date_to_pay = line.date_maturity
date_to_pay = (
line.date_maturity
if line.date_maturity and line.date_maturity > _today
else False)
### end account banking
# end account banking
elif payment.date_prefered == 'fixed':
### account_banking
# account_banking
# date_to_pay = payment.date_scheduled
date_to_pay = (
payment.date_scheduled
if payment.date_scheduled and payment.date_scheduled > _today
else False)
### end account banking
# end account banking
### account_banking
# account_banking
state = 'normal'
communication = line.ref or '-'
if line.invoice:
@@ -132,19 +133,19 @@ class payment_order_create(orm.TransientModel):
state = 'structured'
# support debit orders when enabled
if (payment.payment_order_type == 'debit' and
'amount_to_receive' in line):
if (payment.payment_order_type == 'debit'
and 'amount_to_receive' in line):
amount_currency = line.amount_to_receive
else:
amount_currency = line.amount_to_pay
### end account_banking
# end account_banking
### account banking
# account banking
# t = None
# line2bank = line_obj.line2bank(cr, uid, line_ids, t, context)
line2bank = self.pool['account.move.line'].line2bank(
cr, uid, [line.id], payment.mode.id, context)
### end account banking
# end account banking
res = {
'move_line_id': line.id,
@@ -152,11 +153,11 @@ class payment_order_create(orm.TransientModel):
'bank_id': line2bank.get(line.id),
'order_id': payment.id,
'partner_id': line.partner_id and line.partner_id.id or False,
### account banking
# account banking
# 'communication': line.ref or '/'
'communication': communication,
'state': state,
### end account banking
# end account banking
'date': date_to_pay,
'currency': (line.invoice and line.invoice.currency_id.id
or line.journal_id.currency.id
@@ -166,11 +167,11 @@ class payment_order_create(orm.TransientModel):
def create_payment(self, cr, uid, ids, context=None):
'''
This method is a slightly modified version of the existing method on this
model in account_payment.
This method is a slightly modified version of the existing method on
this model in account_payment.
- pass the payment mode to line2bank()
- allow invoices to create influence on the payment process: not only 'Free'
references are allowed, but others as well
- allow invoices to create influence on the payment process: not only
'Free' references are allowed, but others as well
- check date_to_pay is not in the past.
'''
if context is None:
@@ -182,18 +183,19 @@ class payment_order_create(orm.TransientModel):
payment = self.pool['payment.order'].browse(
cr, uid, context['active_id'], context=context)
## Populate the current payment with new lines:
# Populate the current payment with new lines:
for line in self.pool['account.move.line'].browse(
cr, uid, line_ids, context=context):
vals = self._prepare_payment_line(
cr, uid, payment, line, context=context)
self.pool['payment.line'].create(cr, uid, vals, context=context)
# Force reload of payment order view as a workaround for lp:1155525
return {'name': _('Payment Orders'),
'context': context,
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': 'payment.order',
'res_id': context['active_id'],
'type': 'ir.actions.act_window',
return {
'name': _('Payment Orders'),
'context': context,
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': 'payment.order',
'res_id': context['active_id'],
'type': 'ir.actions.act_window',
}

View File

@@ -30,7 +30,7 @@
'depends': ['account_banking_pain_base'],
'external_dependencies': {
'python': ['unidecode', 'lxml'],
},
},
'data': [
'account_banking_sepa_view.xml',
'wizard/export_sepa_view.xml',
@@ -41,13 +41,23 @@
'description': '''
Module to export payment orders in SEPA XML file format.
SEPA PAIN (PAyment INitiation) is the new european standard for Customer-to-Bank payment instructions. This module implements SEPA Credit Transfer (SCT), more specifically PAIN versions 001.001.02, 001.001.03, 001.001.04 and 001.001.05. It is part of the ISO 20022 standard, available on http://www.iso20022.org.
SEPA PAIN (PAyment INitiation) is the new european standard for
Customer-to-Bank payment instructions.
The Implementation Guidelines for SEPA Credit Transfer published by the European Payments Council (http://http://www.europeanpaymentscouncil.eu) use PAIN version 001.001.03, so it's probably the version of PAIN that you should try first.
This module implements SEPA Credit Transfer (SCT), more specifically PAIN
versions 001.001.02, 001.001.03, 001.001.04 and 001.001.05.
It is part of the ISO 20022 standard, available on http://www.iso20022.org.
This module uses the framework provided by the banking addons, cf https://launchpad.net/banking-addons
The Implementation Guidelines for SEPA Credit Transfer published by the
European Payments Council (http://http://www.europeanpaymentscouncil.eu)
use PAIN version 001.001.03, so it's probably the version of PAIN that you
should try first.
Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com> for any help or question about this module.
This module uses the framework provided by the banking addons,
cf https://www.github.com/OCA/banking-addons
Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com>
for any help or question about this module.
''',
'active': False,
'installable': True,

View File

@@ -30,7 +30,7 @@
'depends': ['account_direct_debit', 'account_banking_pain_base'],
'external_dependencies': {
'python': ['unidecode', 'lxml'],
},
},
'data': [
'security/original_mandate_required_security.xml',
'account_banking_sdd_view.xml',
@@ -49,13 +49,23 @@
'description': '''
Module to export direct debit payment orders in SEPA XML file format.
SEPA PAIN (PAyment INitiation) is the new european standard for Customer-to-Bank payment instructions. This module implements SEPA Direct Debit (SDD), more specifically PAIN versions 008.001.02, 008.001.03 and 008.001.04. It is part of the ISO 20022 standard, available on http://www.iso20022.org.
SEPA PAIN (PAyment INitiation) is the new european standard for
Customer-to-Bank payment instructions.
The Implementation Guidelines for SEPA Direct Debit published by the European Payments Council (http://http://www.europeanpaymentscouncil.eu) use PAIN version 008.001.02. So if you don't know which version your bank supports, you should try version 008.001.02 first.
This module implements SEPA Direct Debit (SDD), more specifically PAIN
versions 008.001.02, 008.001.03 and 008.001.04.
It is part of the ISO 20022 standard, available on http://www.iso20022.org.
This module uses the framework provided by the banking addons, cf https://launchpad.net/banking-addons
The Implementation Guidelines for SEPA Direct Debit published by the European
Payments Council (http://http://www.europeanpaymentscouncil.eu) use PAIN
version 008.001.02. So if you don't know which version your bank supports,
you should try version 008.001.02 first.
Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com> for any help or question about this module.
This module uses the framework provided by the banking addons,
cf https://www.github.com/OCA/banking-addons
Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com>
for any help or question about this module.
''',
'active': False,
'installable': True,

View File

@@ -2,4 +2,4 @@ import test_payment_roundtrip
fast_suite = [
test_payment_roundtrip,
]
]

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright (C) 2013 Therp BV (<http://therp.nl>)
#
#
# 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
@@ -83,7 +83,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
reg('res.users').write(
cr, uid, [uid], {
'company_id': self.company_id})
def setup_chart(self, reg, cr, uid):
"""
Set up the configurable chart of accounts and create periods
@@ -115,7 +115,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
reg('account.fiscalyear').create_period(
cr, uid, [fiscalyear_id])
def setup_payables(self, reg, cr, uid):
def setup_payables(self, reg, cr, uid, context=None):
"""
Set up suppliers and invoice them. Check that the invoices
can be validated properly.
@@ -126,25 +126,29 @@ class TestPaymentRoundtrip(SingleTransactionCase):
'name': 'Supplier 1',
'supplier': True,
'country_id': self.country_id,
'bank_ids': [(0, False, {
'state': 'iban',
'acc_number': 'NL42INGB0000454000',
'bank': self.bank_id,
'bank_bic': 'INGBNL2A',
})],
})
'bank_ids': [
(0, False, {
'state': 'iban',
'acc_number': 'NL42INGB0000454000',
'bank': self.bank_id,
'bank_bic': 'INGBNL2A',
})
],
}, context=context)
supplier2 = partner_model.create(
cr, uid, {
'name': 'Supplier 2',
'supplier': True,
'country_id': self.country_id,
'bank_ids': [(0, False, {
'state': 'iban',
'acc_number': 'NL86INGB0002445588',
'bank': self.bank_id,
'bank_bic': 'INGBNL2A',
})],
})
'bank_ids': [
(0, False, {
'state': 'iban',
'acc_number': 'NL86INGB0002445588',
'bank': self.bank_id,
'bank_bic': 'INGBNL2A',
})
],
}, context=context)
self.payable_id = reg('account.account').search(
cr, uid, [
('company_id', '=', self.company_id),
@@ -158,26 +162,29 @@ class TestPaymentRoundtrip(SingleTransactionCase):
'type': 'in_invoice',
'partner_id': supplier1,
'account_id': self.payable_id,
'invoice_line': [(0, False, {
'name': 'Purchase 1',
'price_unit': 100.0,
'quantity': 1,
'account_id': expense_id,})],
'invoice_line': [
(0, False, {
'name': 'Purchase 1',
'price_unit': 100.0,
'quantity': 1,
'account_id': expense_id,
})
],
'reference_type': 'none',
'supplier_invoice_number': 'INV1',
}
}
self.invoice_ids = [
invoice_model.create(
cr, uid, values, context={
'type': 'in_invoice',
})]
values.update({
'partner_id': supplier2,
'name': 'Purchase 2',
'reference_type': 'structured',
'supplier_invoice_number': 'INV2',
'reference': 'STR2',
})
'partner_id': supplier2,
'name': 'Purchase 2',
'reference_type': 'structured',
'supplier_invoice_number': 'INV2',
'reference': 'STR2',
})
self.invoice_ids.append(
invoice_model.create(
cr, uid, values, context={

View File

@@ -34,16 +34,19 @@
'security/ir.model.access.csv',
],
'description': '''
Module to import HSBC format transation files (S.W.I.F.T MT940) and to export payments for HSBC.net (PAYMUL).
Module to import HSBC format transation files (S.W.I.F.T MT940) and to export
payments for HSBC.net (PAYMUL).
Currently it is targetting UK market, due to country variances of the MT940 and PAYMUL.
Currently it is targetting UK market, due to country variances of the MT940 and
PAYMUL.
It is possible to extend this module to work with HSBC.net in other countries and potentially other banks.
It is possible to extend this module to work with HSBC.net in other countries
and potentially other banks.
This module adds above import/export filter to the account_banking module.
All business logic is in account_banking module.
This module adds above import/export filter to the account_banking module.
All business logic is in account_banking module.
Initial release of this module was co-sponsored by Canonical.
''',
Initial release of this module was co-sponsored by Canonical.
''',
'installable': True,
}

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -19,12 +19,14 @@
#
##############################################################################
from osv import osv, fields
from datetime import date
from tools.translate import _
class hsbc_export(osv.osv):
'''HSBC Export'''
from openerp.osv import orm, fields
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as OE_DATEFORMAT
class hsbc_export(orm.Model):
"""HSBC Export"""
_name = 'banking.export.hsbc'
_description = __doc__
_rec_name = 'execution_date'
@@ -39,7 +41,7 @@ class hsbc_export(osv.osv):
'identification':
fields.char('Identification', size=15, readonly=True, select=True),
'execution_date':
fields.date('Execution Date',readonly=True),
fields.date('Execution Date', readonly=True),
'no_transactions':
fields.integer('Number of Transactions', readonly=True),
'total_amount':
@@ -57,51 +59,53 @@ class hsbc_export(osv.osv):
}
_defaults = {
'date_generated': lambda *a: date.today().strftime('%Y-%m-%d'),
'state': lambda *a: 'draft',
'date_generated': lambda *a: date.today().strftime(OE_DATEFORMAT),
'state': 'draft',
}
hsbc_export()
class payment_line(osv.osv):
'''
The standard payment order is using a mixture of details from the partner record
and the res.partner.bank record. For, instance, the account holder name is coming
from the res.partner.bank record, but the company name and address are coming from
the partner address record. This is problematic because the HSBC payment format
is validating for alphanumeric characters in the company name and address. So,
"Great Company Ltd." and "Great Company s.a." will cause an error because they have
full-stops in the name.
class payment_line(orm.Model):
"""The standard payment order is using a mixture of details from the
partner record and the res.partner.bank record. For, instance, the account
holder name is coming from the res.partner.bank record, but the company
name and address are coming from the partner address record. This is
problematic because the HSBC payment format is validating for alphanumeric
characters in the company name and address. So, "Great Company Ltd." and
"Great Company s.a." will cause an error because they have full-stops in
the name.
A better approach is to use the name and address details from the
res.partner.bank record always. This way, the address details can be
sanitized for the payments, whilst being able to print the proper name and
address throughout the rest of the system e.g. on invoices.
"""
A better approach is to use the name and address details from the res.partner.bank
record always. This way, the address details can be sanitized for the payments,
whilst being able to print the proper name and address throughout the rest of the
system e.g. on invoices.
'''
_name = 'payment.line'
_inherit = 'payment.line'
def info_owner(self, cr, uid, ids, name=None, args=None, context=None):
if not ids: return {}
if not ids:
return {}
result = {}
info=''
info = ''
for line in self.browse(cr, uid, ids, context=context):
owner = line.order_id.mode.bank_id
name = owner.owner_name or owner.partner_id.name
st = owner.street and owner.street or ''
st1 = '' #no street2 in res.partner.bank
st1 = '' # no street2 in res.partner.bank
zip = owner.zip and owner.zip or ''
city = owner.city and owner.city or ''
city = owner.city and owner.city or ''
zip_city = zip + ' ' + city
cntry = owner.country_id and owner.country_id.name or ''
info = name + "\n" + st + " " + st1 + "\n" + zip_city + "\n" +cntry
info = name + "\n".join((st + " ", st1, zip_city, cntry))
result[line.id] = info
return result
def info_partner(self, cr, uid, ids, name=None, args=None, context=None):
if not ids: return {}
if not ids:
return {}
result = {}
info = ''
@@ -110,24 +114,28 @@ class payment_line(osv.osv):
name = partner.owner_name or partner.partner_id.name
st = partner.street and partner.street or ''
st1 = '' #no street2 in res.partner.bank
st1 = '' # no street2 in res.partner.bank
zip = partner.zip and partner.zip or ''
city = partner.city and partner.city or ''
city = partner.city and partner.city or ''
zip_city = zip + ' ' + city
cntry = partner.country_id and partner.country_id.name or ''
info = name + "\n" + st + " " + st1 + "\n" + zip_city + "\n" +cntry
info = name + "\n".join((st + " ", st1, zip_city, cntry))
result[line.id] = info
return result
# Define the info_partner and info_owner so we can override the methods
_columns = {
'info_owner': fields.function(info_owner, string="Owner Account", type="text", help='Address of the Main Partner'),
'info_partner': fields.function(info_partner, string="Destination Account", type="text", help='Address of the Ordering Customer.'),
'info_owner': fields.function(
info_owner,
string="Owner Account",
type="text",
help='Address of the Main Partner',
),
'info_partner': fields.function(
info_partner,
string="Destination Account",
type="text",
help='Address of the Ordering Customer.'
),
}
payment_line()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -1,7 +1,8 @@
# -*- encoding: utf-8 -*-
from osv import osv, fields
from openerp.osv import orm, fields
class hsbc_clientid(osv.osv):
class hsbc_clientid(orm.Model):
"""
Record to hold the HSBCNet Client ID for the company.
"""
@@ -11,38 +12,38 @@ class hsbc_clientid(osv.osv):
_columns = {
'name': fields.char('Name', size=64, required=True),
'clientid': fields.char('Client ID', size=20, required=True),
'company_id': fields.many2one('res.company','Company', required=True),
}
'company_id': fields.many2one('res.company', 'Company', required=True),
}
_defaults = {
'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
}
hsbc_clientid()
'company_id': (
lambda self, cr, uid, c:
self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
}
class payment_order(osv.osv):
_name = 'payment.order'
class payment_order(orm.Model):
_inherit = 'payment.order'
_columns = {
'hsbc_clientid_id': fields.many2one('banking.hsbc.clientid', 'HSBC Client ID', required=True),
}
'hsbc_clientid_id': fields.many2one(
'banking.hsbc.clientid',
'HSBC Client ID',
required=True,
),
}
def _default_hsbc_clientid(self, cr, uid, context=None):
company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
clientid_ids = self.pool.get('banking.hsbc.clientid').search(cr, uid, [('company_id','=',company_id)])
if len(clientid_ids)==0:
user = self.pool['res.users'].browse(cr, uid, uid, context=context)
company_id = user.company_id.id
clientid_ids = self.pool['banking.hsbc.clientid'].search(
cr, uid, [('company_id', '=', company_id)]
)
if len(clientid_ids) == 0:
return False
else:
return clientid_ids[0]
_defaults = {
'hsbc_clientid_id':_default_hsbc_clientid,
}
payment_order()
'hsbc_clientid_id': _default_hsbc_clientid,
}

View File

@@ -22,29 +22,32 @@
#
from account_banking.parsers import models
from tools.translate import _
from mt940_parser import HSBCParser
import re
import osv
import logging
bt = models.mem_bank_transaction
logger = logging.getLogger('hsbc_mt940')
from openerp.tools.translate import _
from openerp.osv import orm
def record2float(record, value):
if record['creditmarker'][-1] == 'C':
return float(record[value])
return -float(record[value])
class transaction(models.mem_bank_transaction):
mapping = {
'execution_date' : 'valuedate',
'value_date' : 'valuedate',
'local_currency' : 'currency',
'transfer_type' : 'bookingcode',
'reference' : 'custrefno',
'message' : 'furtherinfo'
'execution_date': 'valuedate',
'value_date': 'valuedate',
'local_currency': 'currency',
'transfer_type': 'bookingcode',
'reference': 'custrefno',
'message': 'furtherinfo'
}
type_map = {
@@ -60,13 +63,13 @@ class transaction(models.mem_bank_transaction):
'''
super(transaction, self).__init__(*args, **kwargs)
for key, value in self.mapping.iteritems():
if record.has_key(value):
if value in record:
setattr(self, key, record[value])
self.transferred_amount = record2float(record, 'amount')
# Set the transfer type based on the bookingcode
if record.get('bookingcode','ignore') in self.type_map:
if record.get('bookingcode', 'ignore') in self.type_map:
self.transfer_type = self.type_map[record['bookingcode']]
else:
# Default to the generic order, so it will be eligible for matching
@@ -74,6 +77,7 @@ class transaction(models.mem_bank_transaction):
if not self.is_valid():
logger.info("Invalid: %s", record)
def is_valid(self):
'''
We don't have remote_account so override base
@@ -81,6 +85,7 @@ class transaction(models.mem_bank_transaction):
return (self.execution_date
and self.transferred_amount and True) or False
class statement(models.mem_bank_statement):
'''
Bank statement imported data
@@ -89,35 +94,44 @@ class statement(models.mem_bank_statement):
def import_record(self, record):
def _transmission_number():
self.id = record['transref']
def _account_number():
# The wizard doesn't check for sort code
self.local_account = record['sortcode'] + ' ' + record['accnum'].zfill(8)
self.local_account = (
record['sortcode'] + ' ' + record['accnum'].zfill(8)
)
def _statement_number():
self.id = '-'.join([self.id, self.local_account, record['statementnr']])
self.id = '-'.join(
[self.id, self.local_account, record['statementnr']]
)
def _opening_balance():
self.start_balance = record2float(record,'startingbalance')
self.start_balance = record2float(record, 'startingbalance')
self.local_currency = record['currencycode']
def _closing_balance():
self.end_balance = record2float(record, 'endingbalance')
self.date = record['bookingdate']
def _transaction_new():
self.transactions.append(transaction(record))
def _transaction_info():
self.transaction_info(record)
def _not_used():
logger.info("Didn't use record: %s", record)
rectypes = {
'20' : _transmission_number,
'25' : _account_number,
'28' : _statement_number,
'20': _transmission_number,
'25': _account_number,
'28': _statement_number,
'28C': _statement_number,
'60F': _opening_balance,
'62F': _closing_balance,
#'64' : _forward_available,
#'62M': _interim_balance,
'61' : _transaction_new,
'86' : _transaction_info,
'61': _transaction_new,
'86': _transaction_info,
}
rectypes.get(record['recordid'], _not_used)()
@@ -128,15 +142,28 @@ class statement(models.mem_bank_statement):
'''
# Additional information for previous transaction
if len(self.transactions) < 1:
logger.info("Received additional information for non existent transaction:")
logger.info(
"Received additional information for non existent transaction:"
)
logger.info(record)
else:
transaction = self.transactions[-1]
transaction.id = ','.join([record[k] for k in ['infoline{0}'.format(i) for i in range(2,5)] if record.has_key(k)])
transaction.id = ','.join((
record[k]
for k in (
'infoline{0}'.format(i)
for i in range(2, 5)
)
if k in record
))
def raise_error(message, line):
raise osv.osv.except_osv(_('Import error'),
'Error in import:%s\n\n%s' % (message, line))
raise orm.except_orm(
_('Import error'),
_('Error in import:') + '\n\n'.join((message, line))
)
class parser_hsbc_mt940(models.parser):
code = 'HSBC-MT940'
@@ -153,14 +180,19 @@ class parser_hsbc_mt940(models.parser):
# Split into statements
statements = [st for st in re.split('[\r\n]*(?=:20:)', data)]
# Split by records
statement_list = [re.split('[\r\n ]*(?=:\d\d[\w]?:)', st) for st in statements]
statement_list = [
re.split('[\r\n ]*(?=:\d\d[\w]?:)', st)
for st in statements
]
for statement_lines in statement_list:
stmnt = statement()
records = [parser.parse_record(record) for record in statement_lines]
records = [
parser.parse_record(record)
for record in statement_lines
]
[stmnt.import_record(r) for r in records if r is not None]
if stmnt.is_valid():
result.append(stmnt)
else:
@@ -168,5 +200,3 @@ class parser_hsbc_mt940(models.parser):
logger.info(records[0])
return result
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -27,21 +27,23 @@ Based on fi_patu's parser
import re
from datetime import datetime
class HSBCParser(object):
def __init__( self ):
def __init__(self):
recparse = dict()
patterns = {'ebcdic': "\w/\?:\(\).,'+{} -"}
# MT940 header
recparse["20"] = ":(?P<recordid>20):(?P<transref>.{1,16})"
recparse["25"] = ":(?P<recordid>25):(?P<sortcode>\d{6})(?P<accnum>\d{1,29})"
recparse["25"] = (":(?P<recordid>25):(?P<sortcode>\d{6})"
"(?P<accnum>\d{1,29})")
recparse["28"] = ":(?P<recordid>28C?):(?P<statementnr>.{1,8})"
# Opening balance 60F
recparse["60F"] = ":(?P<recordid>60F):(?P<creditmarker>[CD])" \
+ "(?P<prevstmtdate>\d{6})(?P<currencycode>.{3})" \
+ "(?P<startingbalance>[\d,]{1,15})"
recparse["60F"] = (":(?P<recordid>60F):(?P<creditmarker>[CD])"
"(?P<prevstmtdate>\d{6})(?P<currencycode>.{3})"
"(?P<startingbalance>[\d,]{1,15})")
# Transaction
recparse["61"] = """\
@@ -58,24 +60,24 @@ class HSBCParser(object):
""" % (patterns)
# Further info
recparse["86"] = ":(?P<recordid>86):" \
+ "(?P<infoline1>.{1,80})?" \
+ "(?:\n(?P<infoline2>.{1,80}))?" \
+ "(?:\n(?P<infoline3>.{1,80}))?" \
+ "(?:\n(?P<infoline4>.{1,80}))?" \
+ "(?:\n(?P<infoline5>.{1,80}))?"
recparse["86"] = (":(?P<recordid>86):"
"(?P<infoline1>.{1,80})?"
"(?:\n(?P<infoline2>.{1,80}))?"
"(?:\n(?P<infoline3>.{1,80}))?"
"(?:\n(?P<infoline4>.{1,80}))?"
"(?:\n(?P<infoline5>.{1,80}))?")
# Forward available balance (64) / Closing balance (62F) / Interim balance (62M)
recparse["64"] = ":(?P<recordid>64|62[FM]):" \
+ "(?P<creditmarker>[CD])" \
+ "(?P<bookingdate>\d{6})(?P<currencycode>.{3})" \
+ "(?P<endingbalance>[\d,]{1,15})"
# Forward available balance (64) / Closing balance (62F)
# / Interim balance (62M)
recparse["64"] = (":(?P<recordid>64|62[FM]):"
"(?P<creditmarker>[CD])"
"(?P<bookingdate>\d{6})(?P<currencycode>.{3})"
"(?P<endingbalance>[\d,]{1,15})")
for record in recparse:
recparse[record] = re.compile(recparse[record])
self.recparse = recparse
def parse_record(self, line):
"""
Parse record using regexps and apply post processing
@@ -85,25 +87,27 @@ class HSBCParser(object):
if matchobj:
break
if not matchobj:
print " **** failed to match line '%s'" % (line)
print(" **** failed to match line '%s'" % (line))
return
# Strip strings
matchdict = matchobj.groupdict()
# Remove members set to None
matchdict=dict([(k,v) for k,v in matchdict.iteritems() if v])
matchdict = dict([(k, v) for k, v in matchdict.iteritems() if v])
matchkeys = set(matchdict.keys())
needstrip = set(["transref", "accnum", "statementnr", "custrefno",
needstrip = set([
"transref", "accnum", "statementnr", "custrefno",
"bankref", "furtherinfo", "infoline1", "infoline2", "infoline3",
"infoline4", "infoline5", "startingbalance", "endingbalance"])
"infoline4", "infoline5", "startingbalance", "endingbalance"
])
for field in matchkeys & needstrip:
matchdict[field] = matchdict[field].strip()
# Convert to float. Comma is decimal separator
needsfloat = set(["startingbalance", "endingbalance", "amount"])
for field in matchkeys & needsfloat:
matchdict[field] = float(matchdict[field].replace(',','.'))
matchdict[field] = float(matchdict[field].replace(',', '.'))
# Convert date fields
needdate = set(["prevstmtdate", "valuedate", "bookingdate"])
@@ -111,14 +115,18 @@ class HSBCParser(object):
datestring = matchdict[field]
post_check = False
if len(datestring) == 4 and field=="bookingdate" and matchdict.has_key("valuedate"):
if (len(datestring) == 4
and field == "bookingdate"
and "valuedate" in matchdict):
# Get year from valuedate
datestring = matchdict['valuedate'].strftime('%y') + datestring
post_check = True
try:
matchdict[field] = datetime.strptime(datestring,'%y%m%d')
matchdict[field] = datetime.strptime(datestring, '%y%m%d')
if post_check and matchdict[field] > matchdict["valuedate"]:
matchdict[field]=matchdict[field].replace(year=matchdict[field].year-1)
matchdict[field] = matchdict[field].replace(
year=matchdict[field].year-1
)
except ValueError:
matchdict[field] = None
@@ -141,9 +149,11 @@ class HSBCParser(object):
return output
def parse_file(filename):
hsbcfile = open(filename, "r")
p = HSBCParser().parse(hsbcfile.readlines())
with open(filename, "r") as hsbcfile:
HSBCParser().parse(hsbcfile.readlines())
def main():
"""The main function, currently just calls a dummy filename

View File

@@ -6,8 +6,8 @@
# 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
# 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,
@@ -20,4 +20,4 @@
#
##############################################################################
import export_hsbc
from . import export_hsbc

View File

@@ -6,8 +6,8 @@
# 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
# 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,
@@ -22,23 +22,28 @@
import base64
from datetime import datetime, date
from osv import osv, fields
from tools.translate import _
from decimal import Decimal
import paymul
import string
import random
import logging
from openerp.osv import orm, fields
from openerp.tools import ustr
from openerp.tools.translate import _
def strpdate(arg, format='%Y-%m-%d'):
'''shortcut'''
"""shortcut"""
return datetime.strptime(arg, format).date()
def strfdate(arg, format='%Y-%m-%d'):
'''shortcut'''
"""shortcut"""
return arg.strftime(format)
class banking_export_hsbc_wizard(osv.osv_memory):
class banking_export_hsbc_wizard(orm.TransientModel):
_name = 'banking.export.hsbc.wizard'
_description = 'HSBC Export'
_columns = {
@@ -62,9 +67,9 @@ class banking_export_hsbc_wizard(osv.osv_memory):
help=('This is the date the file should be processed by the bank. '
'Don\'t choose a date beyond the nearest date in your '
'payments. The latest allowed date is 30 days from now.\n'
'Please keep in mind that banks only execute on working days '
'and typically use a delay of two days between execution date '
'and effective transfer date.'
'Please keep in mind that banks only execute on working '
'days and typically use a delay of two days between '
'execution date and effective transfer date.'
),
),
'file_id': fields.many2one(
@@ -107,7 +112,7 @@ class banking_export_hsbc_wizard(osv.osv_memory):
from the context.
'''
if not 'execution_date_create' in wizard_data:
if 'execution_date_create' not in wizard_data:
po_ids = context.get('active_ids', [])
po_model = self.pool.get('payment.order')
pos = po_model.browse(cursor, uid, po_ids)
@@ -120,7 +125,9 @@ class banking_export_hsbc_wizard(osv.osv_memory):
elif po.date_prefered == 'due':
for line in po.line_ids:
if line.move_line_id.date_maturity:
date_maturity = strpdate(line.move_line_id.date_maturity)
date_maturity = strpdate(
line.move_line_id.date_maturity
)
if date_maturity < execution_date:
execution_date = date_maturity
@@ -139,8 +146,10 @@ class banking_export_hsbc_wizard(osv.osv_memory):
return super(banking_export_hsbc_wizard, self).create(
cursor, uid, wizard_data, context)
def _create_account(self, oe_account, origin_country=None, is_origin_account=False):
currency = None # let the receiving bank select the currency from the batch
def _create_account(self, oe_account, origin_country=None,
is_origin_account=False):
# let the receiving bank select the currency from the batch
currency = None
holder = oe_account.owner_name or oe_account.partner_id.name
self.logger.info('Create account %s' % (holder))
self.logger.info('-- %s' % (oe_account.country_id.code))
@@ -158,12 +167,13 @@ class banking_export_hsbc_wizard(osv.osv_memory):
'charges': paymul.CHARGES_EACH_OWN,
}
elif oe_account.country_id.code == 'GB':
self.logger.info('GB: %s %s' % (oe_account.country_id.code,oe_account.acc_number))
self.logger.info('GB: %s %s' % (oe_account.country_id.code,
oe_account.acc_number))
split = oe_account.acc_number.split(" ", 2)
if len(split) == 2:
sortcode, accountno = split
else:
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
"Invalid GB acccount number '%s'" % oe_account.acc_number)
paymul_account = paymul.UKAccount(
@@ -175,15 +185,17 @@ class banking_export_hsbc_wizard(osv.osv_memory):
transaction_kwargs = {
'charges': paymul.CHARGES_PAYEE,
}
elif oe_account.country_id.code in ('US','CA'):
self.logger.info('US/CA: %s %s' % (oe_account.country_id.code,oe_account.acc_number))
elif oe_account.country_id.code in ('US', 'CA'):
self.logger.info('US/CA: %s %s' % (oe_account.country_id.code,
oe_account.acc_number))
split = oe_account.acc_number.split(' ', 2)
if len(split) == 2:
sortcode, accountno = split
else:
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
"Invalid %s account number '%s'" % (oe_account.country_id.code,oe_account.acc_number))
_("Invalid %s account number '%s'") %
(oe_account.country_id.code, oe_account.acc_number))
paymul_account = paymul.NorthAmericanAccount(
number=accountno,
sortcode=sortcode,
@@ -201,14 +213,15 @@ class banking_export_hsbc_wizard(osv.osv_memory):
'charges': paymul.CHARGES_PAYEE,
}
else:
self.logger.info('SWIFT Account: %s' % (oe_account.country_id.code))
self.logger.info('SWIFT Account: %s' % oe_account.country_id.code)
split = oe_account.acc_number.split(' ', 2)
if len(split) == 2:
sortcode, accountno = split
else:
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
"Invalid %s account number '%s'" % (oe_account.country_id.code,oe_account.acc_number))
_("Invalid %s account number '%s'") %
(oe_account.country_id.code, oe_account.acc_number))
paymul_account = paymul.SWIFTAccount(
number=accountno,
sortcode=sortcode,
@@ -229,25 +242,35 @@ class banking_export_hsbc_wizard(osv.osv_memory):
def _create_transaction(self, line):
# Check on missing partner of bank account (this can happen!)
if not line.bank_id or not line.bank_id.partner_id:
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
_('There is insufficient information.\r\n'
'Both destination address and account '
'number must be provided'
)
'Both destination address and account '
'number must be provided')
)
self.logger.info('====')
dest_account, transaction_kwargs = self._create_account(line.bank_id, line.order_id.mode.bank_id.country_id.code)
means = {'ACH or EZONE': paymul.MEANS_ACH_OR_EZONE,
'Faster Payment': paymul.MEANS_FASTER_PAYMENT,
'Priority Payment': paymul.MEANS_PRIORITY_PAYMENT}.get(line.order_id.mode.type.name)
self.logger.info('====')
dest_account, transaction_kwargs = self._create_account(
line.bank_id, line.order_id.mode.bank_id.country_id.code
)
means = {
'ACH or EZONE': paymul.MEANS_ACH_OR_EZONE,
'Faster Payment': paymul.MEANS_FASTER_PAYMENT,
'Priority Payment': paymul.MEANS_PRIORITY_PAYMENT
}.get(line.order_id.mode.type.name)
if means is None:
raise osv.except_osv('Error', "Invalid payment type mode for HSBC '%s'" % line.order_id.mode.type.name)
raise orm.except_orm(
_('Error'),
_("Invalid payment type mode for HSBC '%s'")
% line.order_id.mode.type.name
)
if not line.info_partner:
raise osv.except_osv('Error', "No default address for transaction '%s'" % line.name)
raise orm.except_orm(
_('Error'),
_("No default address for transaction '%s'") % line.name
)
try:
return paymul.Transaction(
@@ -261,9 +284,9 @@ class banking_export_hsbc_wizard(osv.osv_memory):
**transaction_kwargs
)
except ValueError as exc:
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
_('Transaction invalid: ') + str(exc)
_('Transaction invalid: %s') + ustr(exc)
)
def wizard_export(self, cursor, uid, wizard_data_ids, context):
@@ -275,23 +298,29 @@ class banking_export_hsbc_wizard(osv.osv_memory):
result_model = self.pool.get('banking.export.hsbc')
payment_orders = wizard_data.payment_order_ids
try:
self.logger.info('Source - %s (%s) %s' % (payment_orders[0].mode.bank_id.partner_id.name, payment_orders[0].mode.bank_id.acc_number, payment_orders[0].mode.bank_id.country_id.code))
self.logger.info(
'Source - %s (%s) %s' % (
payment_orders[0].mode.bank_id.partner_id.name,
payment_orders[0].mode.bank_id.acc_number,
payment_orders[0].mode.bank_id.country_id.code)
)
src_account = self._create_account(
payment_orders[0].mode.bank_id, payment_orders[0].mode.bank_id.country_id.code, is_origin_account=True
payment_orders[0].mode.bank_id,
payment_orders[0].mode.bank_id.country_id.code,
is_origin_account=True
)[0]
except ValueError as exc:
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
_('Source account invalid: ') + str(exc)
_('Source account invalid: ') + ustr(exc)
)
if not isinstance(src_account, paymul.UKAccount):
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
_("Your company's bank account has to have a valid UK "
"account number (not IBAN)" + str(type(src_account)))
"account number (not IBAN)" + ustr(type(src_account)))
)
try:
@@ -299,7 +328,9 @@ class banking_export_hsbc_wizard(osv.osv_memory):
transactions = []
hsbc_clientid = ''
for po in payment_orders:
transactions += [self._create_transaction(l) for l in po.line_ids]
transactions += [
self._create_transaction(l) for l in po.line_ids
]
hsbc_clientid = po.hsbc_clientid_id.clientid
batch = paymul.Batch(
@@ -310,9 +341,9 @@ class banking_export_hsbc_wizard(osv.osv_memory):
)
batch.transactions = transactions
except ValueError as exc:
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
_('Batch invalid: ') + str(exc)
_('Batch invalid: ') + ustr(exc)
)
# Generate random identifier until an unused one is found
@@ -347,7 +378,7 @@ class banking_export_hsbc_wizard(osv.osv_memory):
self.write(cursor, uid, [wizard_data_ids[0]], {
'file_id': file_id,
'no_transactions' : len(batch.transactions),
'no_transactions': len(batch.transactions),
'state': 'finish',
}, context)
@@ -389,13 +420,9 @@ class banking_export_hsbc_wizard(osv.osv_memory):
po_model = self.pool.get('payment.order')
result_model.write(cursor, uid, [wizard_data.file_id.id],
{'state':'sent'})
{'state': 'sent'})
po_ids = [po.id for po in wizard_data.payment_order_ids]
po_model.action_sent(cursor, uid, po_ids)
return {'type': 'ir.actions.act_window_close'}
banking_export_hsbc_wizard()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -25,8 +25,14 @@ import datetime
import re
import unicodedata
from openerp.tools import ustr
def strip_accents(string):
return unicodedata.normalize('NFKD', unicode(string)).encode('ASCII', 'ignore')
res = unicodedata.normalize('NFKD', ustr(string))
res = res.encode('ASCII', 'ignore')
return res
def split_account_holder(holder):
holder_parts = holder.split("\n")
@@ -38,17 +44,20 @@ def split_account_holder(holder):
return holder_parts[0], line2
def address_truncate(name_address):
addr_line = name_address.upper().split("\n")[0:5]
addr_line = [s[:35] for s in addr_line]
return addr_line
"""
The standard says alphanumeric characters, but spaces are also allowed
"""
def edifact_isalnum(s):
"""The standard says alphanumeric characters, but spaces are also
allowed
"""
return bool(re.match(r'^[A-Za-z0-9 ]*$', s))
def edifact_digits(val, digits=None, mindigits=None):
if digits is None:
digits = ''
@@ -58,10 +67,12 @@ def edifact_digits(val, digits=None, mindigits=None):
pattern = r'^[0-9]{' + str(mindigits) + ',' + str(digits) + r'}$'
return bool(re.match(pattern, str(val)))
def edifact_isalnum_size(val, digits):
pattern = r'^[A-Za-z0-9 ]{' + str(digits) + ',' + str(digits) + r'}$'
return bool(re.match(pattern, str(val)))
class HasCurrency(object):
def _get_currency(self):
return self._currency
@@ -71,12 +82,13 @@ class HasCurrency(object):
self._currency = None
else:
if not len(currency) <= 3:
raise ValueError("Currency must be <= 3 characters long: " +
str(currency))
raise ValueError("Currency must be <= 3 characters long: %s" %
ustr(currency))
if not edifact_isalnum(currency):
raise ValueError("Currency must be alphanumeric: " + str(currency))
raise ValueError("Currency must be alphanumeric: %s" %
ustr(currency))
self._currency = currency.upper()
currency = property(_get_currency, _set_currency)
@@ -88,17 +100,19 @@ class LogicalSection(object):
segments = self.segments()
def format_segment(segment):
return '+'.join([':'.join([str(strip_accents(y)) for y in x]) for x in segment]) + "'"
return '+'.join(
[':'.join([str(strip_accents(y)) for y in x]) for x in segment]
) + "'"
return "\n".join([format_segment(s) for s in segments])
def _fii_segment(self, party_qualifier):
holder = split_account_holder(self.holder)
account_identification = [self.number.replace(' ',''), holder[0]]
account_identification = [self.number.replace(' ', ''), holder[0]]
if holder[1] or self.currency:
account_identification.append(holder[1])
if self.currency:
if self.currency:
account_identification.append(self.currency)
return [
['FII'],
@@ -127,8 +141,8 @@ class UKAccount(HasCurrency):
def _set_sortcode(self, sortcode):
if not edifact_digits(sortcode, 6):
raise ValueError("Account sort code must be 6 digits long: " +
str(sortcode))
raise ValueError("Account sort code must be 6 digits long: %s" %
ustr(sortcode))
self._sortcode = sortcode
@@ -141,16 +155,20 @@ class UKAccount(HasCurrency):
holder_parts = split_account_holder(holder)
if not len(holder_parts[0]) <= 35:
raise ValueError("Account holder must be <= 35 characters long: " + str(holder_parts[0]))
raise ValueError("Account holder must be <= 35 characters long: %s"
% ustr(holder_parts[0]))
if not len(holder_parts[1]) <= 35:
raise ValueError("Second line of account holder must be <= 35 characters long: " + str(holder_parts[1]))
raise ValueError("Second line of account holder must be <= 35 "
"characters long: %s" % ustr(holder_parts[1]))
if not edifact_isalnum(holder_parts[0]):
raise ValueError("Account holder must be alphanumeric: " + str(holder_parts[0]))
raise ValueError("Account holder must be alphanumeric: %s" %
ustr(holder_parts[0]))
if not edifact_isalnum(holder_parts[1]):
raise ValueError("Second line of account holder must be alphanumeric: " + str(holder_parts[1]))
raise ValueError("Second line of account holder must be "
"alphanumeric: %s" % ustr(holder_parts[1]))
self._holder = holder.upper()
@@ -174,7 +192,7 @@ class UKAccount(HasCurrency):
class NorthAmericanAccount(UKAccount):
def _set_account_ident(self):
if self.origin_country in ('US','CA'):
if self.origin_country in ('US', 'CA'):
# Use the routing number
account_ident = ['', '', '', self.sortcode, 155, 114]
else:
@@ -188,9 +206,9 @@ class NorthAmericanAccount(UKAccount):
else:
expected_digits = 9
if not edifact_digits(sortcode, expected_digits):
raise ValueError("Account routing number must be %d digits long: %s" %
(expected_digits, str(sortcode)))
raise ValueError("Account routing number must be %d digits long: "
"%s" % (expected_digits, ustr(sortcode)))
self._sortcode = sortcode
def _get_sortcode(self):
@@ -199,9 +217,10 @@ class NorthAmericanAccount(UKAccount):
sortcode = property(_get_sortcode, _set_sortcode)
def _set_bic(self, bic):
if not edifact_isalnum_size(bic, 8) and not edifact_isalnum_size(bic, 11):
raise ValueError("Account BIC/Swift code must be 8 or 11 characters long: " +
str(bic))
if (not edifact_isalnum_size(bic, 8)
and not edifact_isalnum_size(bic, 11)):
raise ValueError("Account BIC/Swift code must be 8 or 11 "
"characters long: %s" % ustr(bic))
self._bic = bic
def _get_bic(self):
@@ -211,8 +230,7 @@ class NorthAmericanAccount(UKAccount):
def _set_number(self, number):
if not edifact_digits(number, mindigits=1):
raise ValueError("Account number is invalid: " +
str(number))
raise ValueError("Account number is invalid: %s" % ustr(number))
self._number = number
@@ -221,7 +239,8 @@ class NorthAmericanAccount(UKAccount):
number = property(_get_number, _set_number)
def __init__(self, number, holder, currency, sortcode, swiftcode, country, origin_country=None, is_origin_account=False):
def __init__(self, number, holder, currency, sortcode, swiftcode, country,
origin_country=None, is_origin_account=False):
self.origin_country = origin_country
self.is_origin_account = is_origin_account
self.number = number
@@ -248,9 +267,10 @@ class SWIFTAccount(UKAccount):
sortcode = property(_get_sortcode, _set_sortcode)
def _set_bic(self, bic):
if not edifact_isalnum_size(bic, 8) and not edifact_isalnum_size(bic, 11):
raise ValueError("Account BIC/Swift code must be 8 or 11 characters long: " +
str(bic))
if (not edifact_isalnum_size(bic, 8)
and not edifact_isalnum_size(bic, 11)):
raise ValueError("Account BIC/Swift code must be 8 or 11 "
"characters long: %s" % ustr(bic))
self._bic = bic
def _get_bic(self):
@@ -260,8 +280,8 @@ class SWIFTAccount(UKAccount):
def _set_number(self, number):
if not edifact_digits(number, mindigits=1):
raise ValueError("Account number is invalid: " +
str(number))
raise ValueError("Account number is invalid: %s" %
ustr(number))
self._number = number
@@ -270,7 +290,8 @@ class SWIFTAccount(UKAccount):
number = property(_get_number, _set_number)
def __init__(self, number, holder, currency, sortcode, swiftcode, country, origin_country=None, is_origin_account=False):
def __init__(self, number, holder, currency, sortcode, swiftcode, country,
origin_country=None, is_origin_account=False):
self.origin_country = origin_country
self.is_origin_account = is_origin_account
self.number = number
@@ -289,7 +310,7 @@ class IBANAccount(HasCurrency):
def _set_iban(self, iban):
iban_obj = sepa.IBAN(iban)
if not iban_obj.valid:
raise ValueError("IBAN is invalid: " + str(iban))
raise ValueError("IBAN is invalid: %s" % ustr(iban))
self._iban = iban
self.country = iban_obj.countrycode
@@ -302,21 +323,24 @@ class IBANAccount(HasCurrency):
self.bic = bic
self.currency = currency
self.holder = holder
self.institution_identification = [self.bic, 25, 5, '', '', '' ]
self.institution_identification = [self.bic, 25, 5, '', '', '']
def fii_bf_segment(self):
return _fii_segment(self, 'BF')
class Interchange(LogicalSection):
def _get_reference(self):
return self._reference
def _set_reference(self, reference):
if not len(reference) <= 15:
raise ValueError("Reference must be <= 15 characters long: " + str(reference))
raise ValueError("Reference must be <= 15 characters long: %s" %
ustr(reference))
if not edifact_isalnum(reference):
raise ValueError("Reference must be alphanumeric: " + str(reference))
raise ValueError("Reference must be alphanumeric: %s" %
ustr(reference))
self._reference = reference.upper()
@@ -335,7 +359,8 @@ class Interchange(LogicalSection):
['UNOA', 3],
['', '', self.client_id],
['', '', 'HEXAGON ABC'],
[self.create_dt.strftime('%y%m%d'), self.create_dt.strftime('%H%M')],
[self.create_dt.strftime('%y%m%d'),
self.create_dt.strftime('%H%M')],
[self.reference],
])
segments += self.message.segments()
@@ -353,10 +378,12 @@ class Message(LogicalSection):
def _set_reference(self, reference):
if not len(reference) <= 35:
raise ValueError("Reference must be <= 35 characters long: " + str(reference))
raise ValueError("Reference must be <= 35 characters long: %s" %
ustr(reference))
if not edifact_isalnum(reference):
raise ValueError("Reference must be alphanumeric: " + str(reference))
raise ValueError("Reference must be alphanumeric: %s" %
ustr(reference))
self._reference = reference.upper()
@@ -406,16 +433,19 @@ class Message(LogicalSection):
return segments
class Batch(LogicalSection):
def _get_reference(self):
return self._reference
def _set_reference(self, reference):
if not len(reference) <= 18:
raise ValueError("Reference must be <= 18 characters long: " + str(reference))
raise ValueError("Reference must be <= 18 characters long: %s" %
ustr(reference))
if not edifact_isalnum(reference):
raise ValueError("Reference must be alphanumeric: " + str(reference))
raise ValueError("Reference must be alphanumeric: %s" %
ustr(reference))
self._reference = reference.upper()
@@ -437,7 +467,7 @@ class Batch(LogicalSection):
# Store the payment means
means = None
if len(self.transactions)>0:
if len(self.transactions) > 0:
means = self.transactions[0].means
segments = []
@@ -455,11 +485,12 @@ class Batch(LogicalSection):
['RFF'],
['AEK', self.reference],
])
currencies = set([x.currency for x in self.transactions])
if len(currencies) > 1:
raise ValueError("All transactions in a batch must have the same currency")
raise ValueError("All transactions in a batch must have the "
"same currency")
segments.append([
['MOA'],
[9, self.amount().quantize(Decimal('0.00')), currencies.pop()],
@@ -487,11 +518,12 @@ class Batch(LogicalSection):
['RFF'],
['AEK', self.reference],
])
# Use the transaction amount and currency for the debit line
segments.append([
['MOA'],
[9, transaction.amount.quantize(Decimal('0.00')), transaction.currency],
[9, transaction.amount.quantize(Decimal('0.00')),
transaction.currency],
])
segments.append(self.debit_account.fii_or_segment())
segments.append([
@@ -503,7 +535,7 @@ class Batch(LogicalSection):
use_index = 1
else:
use_index = index + 1
segments += transaction.segments(use_index)
return segments
@@ -518,20 +550,23 @@ CHARGES_PAYEE = 13
CHARGES_EACH_OWN = 14
CHARGES_PAYER = 15
# values per section 2.8.5 "PAI, Payment Instructions" of "HSBC - CRG Paymul Message Implementation Guide"
# values per section 2.8.5 "PAI, Payment Instructions" of
# "HSBC - CRG Paymul Message Implementation Guide"
MEANS_ACH_OR_EZONE = 2
MEANS_PRIORITY_PAYMENT = 52
MEANS_FASTER_PAYMENT = 'FPS'
CHANNEL_INTRA_COMPANY = 'Z24'
class Transaction(LogicalSection, HasCurrency):
def _get_amount(self):
return self._amount
def _set_amount(self, amount):
if len(str(amount)) > 18:
raise ValueError("Amount must be shorter than 18 bytes: " + str(amount))
raise ValueError("Amount must be shorter than 18 bytes: %s" %
ustr(amount))
self._amount = amount
@@ -542,28 +577,41 @@ class Transaction(LogicalSection, HasCurrency):
def _set_payment_reference(self, payment_reference):
if not len(payment_reference) <= 18:
raise ValueError("Payment reference must be <= 18 characters long: " + str(payment_reference))
raise ValueError(
"Payment reference must be <= 18 characters long: %s" %
ustr(payment_reference)
)
if not edifact_isalnum(payment_reference):
raise ValueError("Payment reference must be alphanumeric: " + str(payment_reference))
raise ValueError("Payment reference must be alphanumeric: %s" %
ustr(payment_reference))
self._payment_reference = payment_reference.upper()
payment_reference = property(_get_payment_reference, _set_payment_reference)
payment_reference = property(
_get_payment_reference, _set_payment_reference
)
def _get_customer_reference(self):
return self._customer_reference
def _set_customer_reference(self, customer_reference):
if not len(customer_reference) <= 18:
raise ValueError("Customer reference must be <= 18 characters long: " + str(customer_reference))
raise ValueError(
"Customer reference must be <= 18 characters long: %s" %
ustr(customer_reference)
)
if not edifact_isalnum(customer_reference):
raise ValueError("Customer reference must be alphanumeric: " + str(customer_reference))
raise ValueError("Customer reference must be alphanumeric: %s" %
ustr(customer_reference))
self._customer_reference = customer_reference.upper()
customer_reference = property(_get_customer_reference, _set_customer_reference)
customer_reference = property(
_get_customer_reference,
_set_customer_reference
)
def __init__(self, amount, currency, account, means,
name_address=None, party_name=None, channel='',
@@ -636,4 +684,3 @@ class Transaction(LogicalSection, HasCurrency):
segments.append(nad_segment)
return segments

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -25,15 +25,17 @@ import paymul
from decimal import Decimal
class PaymulTestCase(unittest.TestCase):
def setUp(self):
self.maxDiff = None
def test_uk_high_value_priority_payment(self):
# Changes from spec example: Removed DTM for transaction, HSBC ignores it (section 2.8.3)
expected = \
"""UNB+UNOA:3+::ABC00000001+::HEXAGON ABC+041111:1500+UKHIGHVALUE'
# Changes from spec example: Removed DTM for transaction, HSBC ignores
# it (section 2.8.3)
expected = """\
UNB+UNOA:3+::ABC00000001+::HEXAGON ABC+041111:1500+UKHIGHVALUE'
UNH+1+PAYMUL:D:96A:UN:FUN01G'
BGM+452+UKHIGHVALUE+9'
DTM+137:20041111:102'
@@ -55,46 +57,63 @@ CNT+39:1'
UNT+19+1'
UNZ+1+UKHIGHVALUE'"""
src_account = paymul.UKAccount(number=12345678,
holder='HSBC NET TEST',
currency='GBP',
sortcode=400515)
src_account = paymul.UKAccount(
number=12345678,
holder='HSBC NET TEST',
currency='GBP',
sortcode=400515
)
dest_account = paymul.UKAccount(number=87654321,
holder="XYX LTD FROM FII BF 1\nBEN NAME 2",
currency='GBP',
sortcode=403124)
dest_account = paymul.UKAccount(
number=87654321,
holder="XYX LTD FROM FII BF 1\nBEN NAME 2",
currency='GBP',
sortcode=403124
)
transaction = paymul.Transaction(amount=Decimal('1.00'),
currency='GBP',
account=dest_account,
charges=paymul.CHARGES_PAYEE,
means=paymul.MEANS_PRIORITY_PAYMENT,
channel=paymul.CHANNEL_INTRA_COMPANY,
name_address="SOME BANK PLC\nHSBC NET TEST\nTEST\nTEST\nUNITED KINGDOM",
customer_reference='CRUKHV5',
payment_reference='PQUKHV5')
transaction = paymul.Transaction(
amount=Decimal('1.00'),
currency='GBP',
account=dest_account,
charges=paymul.CHARGES_PAYEE,
means=paymul.MEANS_PRIORITY_PAYMENT,
channel=paymul.CHANNEL_INTRA_COMPANY,
name_address="SOME BANK PLC\n"
"HSBC NET TEST\n"
"TEST\n"
"TEST\n"
"UNITED KINGDOM",
customer_reference='CRUKHV5',
payment_reference='PQUKHV5'
)
batch = paymul.Batch(exec_date=datetime.date(2004, 11, 12),
reference='UKHIGHVALUE',
debit_account=src_account,
name_address="HSBC BANK PLC\nHSBC NET TEST\nTEST\nTEST\nUNITED KINGDOM")
batch = paymul.Batch(
exec_date=datetime.date(2004, 11, 12),
reference='UKHIGHVALUE',
debit_account=src_account,
name_address="HSBC BANK PLC\n"
"HSBC NET TEST\n"
"TEST\n"
"TEST\n"
"UNITED KINGDOM")
batch.transactions.append(transaction)
message = paymul.Message(reference='UKHIGHVALUE',
dt=datetime.datetime(2004, 11, 11))
message.batches.append(batch)
interchange = paymul.Interchange(client_id='ABC00000001',
reference='UKHIGHVALUE',
create_dt=datetime.datetime(2004, 11, 11, 15, 00),
message=message)
interchange = paymul.Interchange(
client_id='ABC00000001',
reference='UKHIGHVALUE',
create_dt=datetime.datetime(2004, 11, 11, 15, 00),
message=message
)
self.assertMultiLineEqual(expected, str(interchange))
def test_ezone(self):
# Changes from example in spec: Changed CNT from 27 to 39, because we only generate that
# and it makes no difference which one we use
# Changes from example in spec: Changed CNT from 27 to 39, because we
# only generate that and it makes no difference which one we use
# Removed DTM for transaction, HSBC ignores it (section 2.8.3)
expected = """UNB+UNOA:3+::ABC12016001+::HEXAGON ABC+080110:0856+EZONE'
@@ -106,33 +125,39 @@ DTM+203:20080114:102'
RFF+AEK:EZONE'
MOA+9:1.00:EUR'
FII+OR+12345678:ACCOUNT HOLDER NAME::EUR+:::403124:154:133+GB'
NAD+OY++ORD PARTY NAME NADOY 01:CRG TC5 001 NADOY ADDRESS LINE 0001:CRG TC5 001 NADOY ADDRESS LINE 0002'
NAD+OY++ORD PARTY NAME NADOY 01:CRG TC5 001 NADOY ADDRESS LINE 0001:CRG TC5 \
1001 NADOY ADDRESS LINE 0002'
SEQ++1'
MOA+9:1.00:EUR'
RFF+CR:EZONE 1A'
RFF+PQ:EZONE 1A'
PAI+::2'
FCA+14'
FII+BF+DE23300308800099990031:CRG TC5 001 BENE NAME FIIBF 000001::EUR+AACSDE33:25:5:::+DE'
NAD+BE+++BENE NAME NADBE T1 001:CRG TC5 001T1 NADBE ADD LINE 1 0001:CRG TC5 001T1 NADBE ADD LINE 2 0001'
FII+BF+DE23300308800099990031:CRG TC5 001 BENE NAME FIIBF \
000001::EUR+AACSDE33:25:5:::+DE'
NAD+BE+++BENE NAME NADBE T1 001:CRG TC5 001T1 NADBE ADD LINE 1 0001:CRG TC5 \
001T1 NADBE ADD LINE 2 0001'
CNT+39:1'
UNT+19+1'
UNZ+1+EZONE'"""
src_account = paymul.UKAccount(
number=12345678,
holder='ACCOUNT HOLDER NAME',
currency='EUR',
sortcode=403124
)
src_account = paymul.UKAccount(number=12345678,
holder='ACCOUNT HOLDER NAME',
currency='EUR',
sortcode=403124)
dest_account = paymul.IBANAccount(
iban="DE23300308800099990031",
holder="CRG TC5 001 BENE NAME FIIBF 000001",
currency='EUR',
bic="AACSDE33"
)
dest_account = paymul.IBANAccount(iban="DE23300308800099990031",
holder="CRG TC5 001 BENE NAME FIIBF 000001",
currency='EUR',
bic="AACSDE33")
party_name = "BENE NAME NADBE T1 001\n" \
+ "CRG TC5 001T1 NADBE ADD LINE 1 0001\n" \
+ "CRG TC5 001T1 NADBE ADD LINE 2 0001"
party_name = ("BENE NAME NADBE T1 001\n"
"CRG TC5 001T1 NADBE ADD LINE 1 0001\n"
"CRG TC5 001T1 NADBE ADD LINE 2 0001")
transaction = paymul.Transaction(amount=Decimal('1.00'),
currency='EUR',
account=dest_account,
@@ -142,9 +167,9 @@ UNZ+1+EZONE'"""
customer_reference='EZONE 1A',
payment_reference='EZONE 1A')
name_address = "ORD PARTY NAME NADOY 01\n" \
+ "CRG TC5 001 NADOY ADDRESS LINE 0001\n" \
+ "CRG TC5 001 NADOY ADDRESS LINE 0002"
name_address = ("ORD PARTY NAME NADOY 01\n"
"CRG TC5 001 NADOY ADDRESS LINE 0001\n"
"CRG TC5 001 NADOY ADDRESS LINE 0002")
batch = paymul.Batch(exec_date=datetime.date(2008, 1, 14),
reference='EZONE',
debit_account=src_account,
@@ -155,23 +180,27 @@ UNZ+1+EZONE'"""
dt=datetime.datetime(2008, 1, 10))
message.batches.append(batch)
interchange = paymul.Interchange(client_id='ABC12016001',
reference='EZONE',
create_dt=datetime.datetime(2008, 1, 10, 8, 56),
message=message)
interchange = paymul.Interchange(
client_id='ABC12016001',
reference='EZONE',
create_dt=datetime.datetime(2008, 1, 10, 8, 56),
message=message
)
self.assertMultiLineEqual(expected, str(interchange))
def test_uk_low_value_ach_instruction_level(self):
dest_account1 = paymul.UKAccount(number=87654321,
holder="HSBC NET RPS TEST\nHSBC BANK",
currency='GBP',
sortcode=403124)
name_address = "HSBC BANK PLC\n" \
+ "PCM\n" \
+ "8CS37\n" \
+ "E14 5HQ\n" \
+ "UNITED KINGDOM"
dest_account1 = paymul.UKAccount(
number=87654321,
holder="HSBC NET RPS TEST\nHSBC BANK",
currency='GBP',
sortcode=403124
)
name_address = ("HSBC BANK PLC\n"
"PCM\n"
"8CS37\n"
"E14 5HQ\n"
"UNITED KINGDOM")
transaction1 = paymul.Transaction(amount=Decimal('1.00'),
currency='GBP',
account=dest_account1,
@@ -181,15 +210,17 @@ UNZ+1+EZONE'"""
customer_reference='CREDIT',
payment_reference='CREDIT')
dest_account2 = paymul.UKAccount(number=12341234,
holder="HSBC NET RPS TEST\nHSBC BANK",
currency='GBP',
sortcode=403124)
name_address = "HSBC BANK PLC\n" \
+ "PCM\n" \
+ "8CS37\n" \
+ "E14 5HQ\n" \
+ "UNITED KINGDOM"
dest_account2 = paymul.UKAccount(
number=12341234,
holder="HSBC NET RPS TEST\nHSBC BANK",
currency='GBP',
sortcode=403124
)
name_address = ("HSBC BANK PLC\n"
"PCM\n"
"8CS37\n"
"E14 5HQ\n"
"UNITED KINGDOM")
transaction2 = paymul.Transaction(amount=Decimal('1.00'),
currency='GBP',
account=dest_account2,
@@ -199,12 +230,11 @@ UNZ+1+EZONE'"""
customer_reference='CREDIT1',
payment_reference='CREDIT1')
name_address = "HSBC BANK PLC\n" \
+ "PCM\n" \
+ "8CS37\n" \
+ "E14 5HQ\n" \
+ "UNITED KINGDOM"
name_address = ("HSBC BANK PLC\n"
"PCM\n"
"8CS37\n"
"E14 5HQ\n"
"UNITED KINGDOM")
src_account = paymul.UKAccount(number=12345678,
holder='BHEX RPS TEST',
@@ -216,23 +246,25 @@ UNZ+1+EZONE'"""
name_address=name_address)
batch.transactions = [transaction1, transaction2]
message = paymul.Message(reference='UKLVPLIL',
dt=datetime.datetime(2004, 11, 11))
message = paymul.Message(
reference='UKLVPLIL',
dt=datetime.datetime(2004, 11, 11)
)
message.batches.append(batch)
interchange = paymul.Interchange(client_id='ABC00000001',
reference='UKLVPLIL',
create_dt=datetime.datetime(2004, 11, 11, 15, 0),
message=message)
interchange = paymul.Interchange(
client_id='ABC00000001',
reference='UKLVPLIL',
create_dt=datetime.datetime(2004, 11, 11, 15, 0),
message=message
)
# Changes from example:
# * Change second transaction from EUR to GBP, because we don't support
# multi-currency batches
# * Removed DTM for transaction, HSBC ignores it (section 2.8.3)
expected = """UNB+UNOA:3+::ABC00000001+::HEXAGON ABC+041111:1500+UKLVPLIL'
expected = """\
UNB+UNOA:3+::ABC00000001+::HEXAGON ABC+041111:1500+UKLVPLIL'
UNH+1+PAYMUL:D:96A:UN:FUN01G'
BGM+452+UKLVPLIL+9'
DTM+137:20041111:102'
@@ -264,11 +296,5 @@ UNZ+1+UKLVPLIL'"""
self.assertMultiLineEqual(expected, str(interchange))
if __name__ == "__main__":
# I ran this with
# env PYTHONPATH=$HOME/src/canonical/hsbc-banking:$HOME/src/openerp/6.0/server/bin:$HOME/src/openerp/6.0/addons python wizard/paymul_test.py
# is there a better way?
unittest.main()

View File

@@ -5,8 +5,8 @@
# 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
# 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,

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright (C) 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -22,6 +22,7 @@
#
##############################################################################
def migrate(cr, version):
if not version:
return

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright (C) 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -25,27 +25,29 @@
import logging
logger = logging.getLogger()
def rename_columns(cr, column_spec):
"""
Rename table columns. Taken from OpenUpgrade.
:param column_spec: a hash with table keys, with lists of tuples as values. \
Tuples consist of (old_name, new_name).
:param column_spec: a hash with table keys, with lists of tuples as \
values. Tuples consist of (old_name, new_name).
"""
for table in column_spec.keys():
for (old, new) in column_spec[table]:
logger.info("table %s, column %s: renaming to %s",
table, old, new)
cr.execute('ALTER TABLE "%s" RENAME "%s" TO "%s"' % (table, old, new,))
logger.info("table %s, column %s: renaming to %s", table, old, new)
cr.execute('ALTER TABLE %s RENAME %s TO %s', (table, old, new,))
cr.execute('DROP INDEX IF EXISTS "%s_%s_index"' % (table, old))
def migrate(cr, version):
if not version:
return
# rename field debit_move_line_id
rename_columns(cr, {
'payment_line': [
('debit_move_line_id', 'banking_addons_61_debit_move_line_id'),
]})
'payment_line': [
('debit_move_line_id', 'banking_addons_61_debit_move_line_id'),
]
})

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -33,13 +33,13 @@ credited afterwards. Such a creditation is called a storno.
Invoice workflow:
1 the sale leads to
1 the sale leads to
1300 Debtors 100
8000 Sales 100
Balance:
Debtors 2000 |
Sales | 2000
Balance:
Debtors 2000 |
Sales | 2000
2 an external booking takes place
1100 Bank 100
@@ -59,11 +59,11 @@ This module implements the following diversion:
2000 Transfer account 100 |
1300 Debtors | 100
Reconciliation takes place between 1 and 2a.
Reconciliation takes place between 1 and 2a.
The invoice gets set to state 'paid', and 'reconciled' = True
Balance:
Debtors 0 |
Debtors 0 |
Transfer account 2000 |
Bank 0 |
Sales | 2000
@@ -76,7 +76,7 @@ Balance:
Reconciliation takes place between 3a and 2a
Balance:
Debtors 0 |
Debtors 0 |
Transfer account 0 |
Bank 2000 |
Sales | 2000
@@ -84,55 +84,57 @@ Balance:
4 a storno from invoice [1] triggers a new booking on the bank account
1300 Debtors 100 |
1100 Bank | 100
Balance:
Debtors 100 |
Debtors 100 |
Transfer account 0 |
Bank 1900 |
Sales | 2000
The reconciliation of 2a is undone. The booking of 2a is reconciled
The reconciliation of 2a is undone. The booking of 2a is reconciled
with the booking of 4 instead.
The payment line attribute 'storno' is set to True and the invoice
state is no longer 'paid'.
Two cases need to be distinguisted:
1) If the storno is a manual storno from the partner, the invoice is set to
state 'debit_denied', with 'reconciled' = False
state 'debit_denied', with 'reconciled' = False
This module implements this option by allowing the bank module to call
netsvc.LocalService("workflow").trg_validate(
uid, 'account.invoice', ids, 'debit_denied', cr)
2) If the storno is an error generated by the bank (assumingly non-fatal),
the invoice is reopened for the next debit run. This is a call to existing
the invoice is reopened for the next debit run.
This is a call to existing
netsvc.LocalService("workflow").trg_validate(
uid, 'account.invoice', ids, 'open_test', cr)
Should also be adding a log entry on the invoice for tracing purposes
self._log_event(cr, uid, ids, -1.0, 'Debit denied')
self._log_event(cr, uid, ids, -1.0, 'Debit denied')
If not for that funny comment
"#TODO: implement messages system" in account/invoice.py
Repeating non-fatal fatal errors need to be dealt with manually by checking
open invoices with a matured invoice- or due date.
"""
"""
class account_invoice(orm.Model):
_inherit = "account.invoice"
def __init__(self, pool, cr):
"""
"""
Adding a state to the hardcoded state list of the inherited
model. The alternative is duplicating the field definition
model. The alternative is duplicating the field definition
in columns but only one module can do that!
Maybe apply a similar trick when overriding the buttons' 'states' attributes
in the form view, manipulating the xml in fields_view_get().
"""
Maybe apply a similar trick when overriding the buttons' 'states'
attributes in the form view, manipulating the xml in fields_view_get().
"""
super(account_invoice, self).__init__(pool, cr)
invoice_obj = pool.get('account.invoice')
invoice_obj._columns['state'].selection.append(
@@ -144,8 +146,8 @@ class account_invoice(orm.Model):
number = self.read(
cr, uid, invoice_id, ['number'], context=context)['number']
raise orm.except_orm(
_('Error !'),
_('You cannot set invoice \'%s\' to state \'debit denied\', ' +
_('Error !'),
_("You cannot set invoice '%s' to state 'debit denied', "
'as it is still reconciled.') % number)
self.write(cr, uid, ids, {'state': 'debit_denied'}, context=context)
for inv_id, name in self.name_get(cr, uid, ids, context=context):
@@ -154,9 +156,9 @@ class account_invoice(orm.Model):
return True
def test_undo_debit_denied(self, cr, uid, ids, context=None):
"""
"""
Called from the workflow. Used to unset paid state on
invoices that were paid with bank transfers which are being cancelled
invoices that were paid with bank transfers which are being cancelled
"""
for invoice in self.read(cr, uid, ids, ['reconciled'], context):
if not invoice['reconciled']:

View File

@@ -23,6 +23,7 @@
from operator import itemgetter
from openerp.osv import fields, orm
class account_move_line(orm.Model):
_inherit = "account.move.line"
@@ -48,7 +49,7 @@ class account_move_line(orm.Model):
AND pl.storno is false
AND po.state != 'cancel') AS amount
FROM account_move_line ml
WHERE id IN %s""", (tuple(ids),))
WHERE id IN %s""", (tuple(ids), ))
r = dict(cr.fetchall())
return r
@@ -80,12 +81,12 @@ class account_move_line(orm.Model):
WHERE type=%s AND active)
AND reconcile_id IS null
AND debit > 0
AND ''' + where + ' and ' + query), ('receivable',)+sql_args )
AND ''' + where + ' and ' + query), ('receivable', ) + sql_args)
res = cr.fetchall()
if not res:
return [('id', '=', '0')]
return [('id', 'in', map(lambda x:x[0], res))]
return [('id', 'in', map(lambda x: x[0], res))]
def line2bank(self, cr, uid, ids, payment_mode_id, context=None):
'''I have to inherit this function for direct debits to fix the

View File

@@ -1,16 +1,15 @@
# -*- coding: utf-8 -*-
from openerp.osv import orm, fields
import netsvc
from tools.translate import _
from openerp.osv import orm
class payment_order(orm.Model):
_inherit = 'payment.order'
def test_undo_done(self, cr, uid, ids, context=None):
"""
"""
Called from the workflow. Used to unset done state on
payment orders that were reconciled with bank transfers
which are being cancelled
which are being cancelled
"""
for order in self.browse(cr, uid, ids, context=context):
if order.payment_order_type == 'debit':

View File

@@ -3,6 +3,7 @@ from openerp.osv import orm, fields
import netsvc
from tools.translate import _
class payment_line(orm.Model):
_inherit = 'payment.line'
@@ -22,15 +23,15 @@ class payment_line(orm.Model):
:param payment_line_id: the single payment line id
:param amount: the (signed) amount debited from the bank account
:param currency: the bank account's currency *browse object*
:param boolean storno_retry: when True, attempt to reopen the invoice, \
set the invoice to 'Debit denied' otherwise.
:param boolean storno_retry: when True, attempt to reopen the \
invoice, set the invoice to 'Debit denied' otherwise.
:return: an incomplete reconcile for the caller to fill
:rtype: database id of an account.move.reconcile resource.
"""
move_line_obj = self.pool.get('account.move.line')
reconcile_obj = self.pool.get('account.move.reconcile')
line = self.browse(cr, uid, payment_line_id)
transit_move_line = line.transit_move_line_id
reconcile_id = False
if (line.transit_move_line_id and not line.storno and
self.pool.get('res.currency').is_zero(
@@ -43,14 +44,13 @@ class payment_line(orm.Model):
# Actually, given the nature of a direct debit order and storno,
# we should not need to take partial into account on the side of
# the transit_move_line.
if line.transit_move_line_id.reconcile_partial_id:
reconcile_id = line.transit_move_line_id.reconcile_partial_id.id
attribute = 'reconcile_partial_id'
if len(line.transit_move_line_id.reconcile_id.line_partial_ids) == 2:
if transit_move_line.reconcile_partial_id:
reconcile_id = transit_move_line.reconcile_partial_id.id
if len(transit_move_line.reconcile_id.line_partial_ids) == 2:
# reuse the simple reconcile for the storno transfer
reconcile_obj.write(
cr, uid, reconcile_id, {
'line_id': [(6, 0, line.transit_move_line_id.id)],
'line_id': [(6, 0, transit_move_line.id)],
'line_partial_ids': [(6, 0, [])],
}, context=context)
else:
@@ -58,27 +58,27 @@ class payment_line(orm.Model):
# and a new one for reconciling the storno transfer
reconcile_obj.write(
cr, uid, reconcile_id, {
'line_partial_ids': [(3, line.transit_move_line_id.id)],
'line_partial_ids': [(3, transit_move_line.id)],
}, context=context)
reconcile_id = reconcile_obj.create(
cr, uid, {
'type': 'auto',
'line_id': [(6, 0, line.transit_move_line_id.id)],
'line_id': [(6, 0, transit_move_line.id)],
}, context=context)
elif line.transit_move_line_id.reconcile_id:
reconcile_id = line.transit_move_line_id.reconcile_id.id
if len(line.transit_move_line_id.reconcile_id.line_id) == 2:
elif transit_move_line.reconcile_id:
reconcile_id = transit_move_line.reconcile_id.id
if len(transit_move_line.reconcile_id.line_id) == 2:
# reuse the simple reconcile for the storno transfer
reconcile_obj.write(
cr, uid, reconcile_id, {
'line_id': [(6, 0, [line.transit_move_line_id.id])]
'line_id': [(6, 0, [transit_move_line.id])]
}, context=context)
else:
# split up the original reconcile in a partial one
# and a new one for reconciling the storno transfer
partial_ids = [
x.id for x in line.transit_move_line_id.reconcile_id.line_id
if x.id != line.transit_move_line_id.id
partial_ids = [
x.id for x in transit_move_line.reconcile_id.line_id
if x.id != transit_move_line.id
]
reconcile_obj.write(
cr, uid, reconcile_id, {
@@ -88,7 +88,7 @@ class payment_line(orm.Model):
reconcile_id = reconcile_obj.create(
cr, uid, {
'type': 'auto',
'line_id': [(6, 0, line.transit_move_line_id.id)],
'line_id': [(6, 0, transit_move_line.id)],
}, context=context)
# mark the payment line for storno processed
if reconcile_id:
@@ -104,7 +104,7 @@ class payment_line(orm.Model):
return reconcile_id
def get_storno_account_id(self, cr, uid, payment_line_id, amount,
currency, context=None):
currency, context=None):
"""
Check the match of the arguments, and return the account associated
with the storno.

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright (C) 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved

View File

@@ -1,7 +1,7 @@
##############################################################################
#
# Copyright (C) 2012 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -39,7 +39,7 @@
This module is compatible with OpenERP 7.0.
The IBAN module in OpenERP 6.1/7.0 registers the IBAN
on the same field as the domestic account number,
on the same field as the domestic account number,
instead of keeping both on separate fields as is the
case in 6.0.

View File

@@ -5,8 +5,8 @@
# 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
# 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,
@@ -27,7 +27,8 @@
'category': 'Banking addons',
'depends': ['account_payment'],
'description': '''
When composing a payment order, select all candidates by default (in the second step of the "Select invoices to pay" wizard).
When composing a payment order, select all candidates by default
(in the second step of the "Select invoices to pay" wizard).
''',
'installable': True,
}

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
@@ -24,11 +24,12 @@
from openerp.osv import orm
class payment_order_create(orm.TransientModel):
_inherit = 'payment.order.create'
def default_get(self, cr, uid, fields_list, context=None):
"""
"""
Automatically add the candidate move lines to
the payment order, instead of only applying them
to the domain.
@@ -39,13 +40,15 @@ class payment_order_create(orm.TransientModel):
been placed in the context at object
creation time.
"""
if context is None:
context = {}
res = super(payment_order_create, self).default_get(
cr, uid, fields_list, context=context)
if (fields_list and 'entries' in fields_list
and 'entries' not in res
and context and context.get('line_ids', False)
):
if (fields_list
and 'entries' in fields_list
and 'entries' not in res
and context.get('line_ids', False)):
res['entries'] = context['line_ids']
return res

Some files were not shown because too many files have changed in this diff Show More