diff --git a/account_statement_base_completion/__openerp__.py b/account_statement_base_completion/__openerp__.py index fb86b19e..f32f29ff 100644 --- a/account_statement_base_completion/__openerp__.py +++ b/account_statement_base_completion/__openerp__.py @@ -29,13 +29,19 @@ 'description': """ The goal of this module is to improve the basic bank statement, help dealing with huge volume of reconciliation by providing basic rules to identify the partner of a bank statement line. - It will also take care of the chosen profile to make his work. + + His goal is to provide an easy way to fullfill the info of a bank statement line based on rules. + The reference of the line is always used by the reconciliation process. We're supposed to copy + there (or write manually) the matching string. That can be : the order Number or an invoice number, + or anything that will be found in the invoice entry part to make the match. + """, 'website': 'http://www.camptocamp.com', 'init_xml': [], 'update_xml': [ 'statement_view.xml', + 'data.xml', ], 'demo_xml': [], 'test': [], diff --git a/account_statement_base_completion/data.xml b/account_statement_base_completion/data.xml new file mode 100644 index 00000000..6a4a29cf --- /dev/null +++ b/account_statement_base_completion/data.xml @@ -0,0 +1,23 @@ + + + + + + Match from line label (based on partner field 'Bank Statement Label') + 60 + get_from_label_and_partner_field + + + + + Match from line label (based on partner name) + 70 + get_from_label_and_partner_name + + + + + + + + diff --git a/account_statement_base_completion/partner.py b/account_statement_base_completion/partner.py index 1dfcbc1e..1c18cf3d 100644 --- a/account_statement_base_completion/partner.py +++ b/account_statement_base_completion/partner.py @@ -51,32 +51,32 @@ class res_partner(osv.osv): # return partner_id and partner_id[0] # return False - def get_partner_from_label_based_on_bank_statement_label(self, cr, uid, label, context=None): - ids = self.search(cr, uid, [['bank_statement_label', '!=', False]], context=context) - for partner in self.browse(cr, uid, ids, context=context): - for partner_label in partner.bank_statement_label.split(';'): - if partner_label in label: - return partner.id - return False - - def get_supplier_partner_from_label_based_on_name(self, cr, uid, label, context=None): - supplier_ids = self.search(cr, uid, [['supplier', '=', True]], context=context) - for partner in self.browse(cr, uid, supplier_ids, context=context): - if partner.name in label: - return partner.id - return False - - def get_partner_account(self, cr, uid, id, amount, context=None): - partner = self.browse(cr, uid, id, context=context) - if partner.supplier and not partner.customer: - return partner.property_account_payable.id - if partner.customer and not partner.supplier: - return partner.property_account_receivable.id - - if amount >0: - return partner.property_account_receivable.id - else: - return partner.property_account_payable.id + # def get_partner_from_label_based_on_bank_statement_label(self, cr, uid, label, context=None): + # ids = self.search(cr, uid, [['bank_statement_label', '!=', False]], context=context) + # for partner in self.browse(cr, uid, ids, context=context): + # for partner_label in partner.bank_statement_label.split(';'): + # if partner_label in label: + # return partner.id + # return False + # + # def get_supplier_partner_from_label_based_on_name(self, cr, uid, label, context=None): + # supplier_ids = self.search(cr, uid, [['supplier', '=', True]], context=context) + # for partner in self.browse(cr, uid, supplier_ids, context=context): + # if partner.name in label: + # return partner.id + # return False + # + # def get_partner_account(self, cr, uid, id, amount, context=None): + # partner = self.browse(cr, uid, id, context=context) + # if partner.supplier and not partner.customer: + # return partner.property_account_payable.id + # if partner.customer and not partner.supplier: + # return partner.property_account_receivable.id + # + # if amount >0: + # return partner.property_account_receivable.id + # else: + # return partner.property_account_payable.id diff --git a/account_statement_base_completion/statement.py b/account_statement_base_completion/statement.py index 859840c8..a502205e 100644 --- a/account_statement_base_completion/statement.py +++ b/account_statement_base_completion/statement.py @@ -23,6 +23,14 @@ import netsvc logger = netsvc.Logger() from openerp.osv.orm import Model, fields + +class ErrorTooManyPartner(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + + class AccountStatementProfil(Model): _inherit = "account.statement.profil" @@ -32,68 +40,124 @@ class AccountStatementProfil(Model): # 'transferts_account_id':fields.many2one('account.account', 'Transferts Account'), 'rule_ids':fields.many2many('account.statement.completion.rule', + string='Related statement profiles', rel='account_statement_rule_statement_profile_to_rel', - 'profile_id','rule_id', - 'Related statement profiles'), + ids1='profile_id',ids2='rule_id', + ), } - def find_partner_by_rules(self, cr, uid, ids, field_value, context=None): + def find_values_from_rules(self, cr, uid, ids, line_id, context=None): """This method will execute all rules, in their sequence order, - to match a partner for the given statement.line and return his id.""" + to match a partner for the given statement.line and return his id. + + :param int/long line_id: eee + :return: A dict of value that can be passed directly to the write method of + the statement line: + {'partner_id': value, + 'account_id' : value, + ... + } + """ if not context: context={} - partner_id = False + res = {} for profile in self.browse(cr, uid, ids, context=context): for rule in profile.rule_ids: method_to_call = getattr(rule, rule.function_to_call) - partner_id = method_to_call(cr,uid,field_value,context) - if partner_id: - return partner_id - return partner_id + result = method_to_call(cr,uid,line_id,context) + if result: + return res + return res class AccountStatementCompletionRule(Model): """This will represent all the completion method that we can have to fullfill the bank statement. You'll be able to extend them in you own module - and choose those to apply for every statement profile.""" + and choose those to apply for every statement profile. + The goal of a rules is to fullfill at least the partner of the line, but + if possible also the reference because we'll use it in the reconciliation + process. The reference should contain the invoice number or the SO number + """ _name = "account.statement.completion.rule" _order = "sequence asc" - _get_functions = [('get_from_label_and_partner_field', 'From line label (based on partner field)'),\ - ('in', 'External -> OpenERP'), ('out', 'External <- OpenERP')] + def _get_functions(self): + """List of available methods for rules. Override this to add you own.""" + return [ + ('get_from_label_and_partner_field', 'From line label (based on partner field)'), + ('get_from_label_and_partner_name', 'From line label (based on partner name)'), + ] _columns={ - 'sequence': fields.integer('Sequence'), - 'name': fields.char('Name') - 'profile_ids':fields.many2many('account.statement.profil', + 'sequence': fields.integer('Sequence', help="Lower means paresed first."), + 'name': fields.char('Name'), + 'profile_ids': fields.many2many('account.statement.profil', rel='account_statement_rule_statement_profile_to_rel', - 'rule_id', 'profile_id', - 'Related statement profiles'), - 'function_to_call': fields.selection(_get_functions, 'Type'), + ids1='rule_id', ids2='profile_id', + string='Related statement profiles'), + 'function_to_call': fields.selection(_get_functions, 'Method'), } - - def get_from_label_and_partner_field(self, cr, uid, field_value, context=None): + + def get_from_label_and_partner_field(self, cr, uid, line_id, context=None): """Match the partner based on the label field of the statement line and the text defined in the 'bank_statement_label' field of the partner. - Remember that we can have values separated with ;""" + Remember that we can have values separated with ; Then, call the generic + st_line method to complete other values. + If more than one partner matched, raise an error. + Return: + A dict of value that can be passed directly to the write method of + the statement line. + {'partner_id': value, + 'account_id' : value, + ...} + """ partner_obj = self.pool.get('res.partner') - ids = partner_obj.search(cr, uid, [['bank_statement_label', '!=', False]], context=context) - for partner in self.browse(cr, uid, ids, context=context): - for partner_label in partner.bank_statement_label.split(';'): - if partner_label in field_value: - return partner.id - return False + st_obj = self.pool.get('account.bank.statement.line') + st_line = st_obj.browse(cr,uid,line_id) + res = {} + compt = 0 + if st_line: + ids = partner_obj.search(cr, uid, [['bank_statement_label', '!=', False]], context=context) + for partner in self.browse(cr, uid, ids, context=context): + for partner_label in partner.bank_statement_label.split(';'): + if partner_label in st_line.label: + compt += 1 + res['partner_id'] = partner.id + if compt > 1: + raise ErrorTooManyPartner(_('Line named "%s" was matched by more than one partner.')%(st_line.name,st_line.id)) + st_vals = st_obj.get_values_for_line(cr, uid, profile_id = st_line.statement_id.profile_id.id, + partner_id = res.get('partner_id',False), line_type = st_line.type, amount = st_line.amount, context = context) + res.update(st_vals) + return res - def get_from_label_and_partner_name(self, cr, uid, field_value, context=None): + def get_from_label_and_partner_name(self, cr, uid, line_id, context=None): """Match the partner based on the label field of the statement line - and the name of the partner.""" - supplier_ids = self.search(cr, uid, [['supplier', '=', True]], context=context) - sql = """SELECT id FROM res_partner WHERE name ilike """ - for partner in self.browse(cr, uid, supplier_ids, context=context): - if partner.name in label: - return partner.id - return False + and the name of the partner. + Then, call the generic st_line method to complete other values. + Return: + A dict of value that can be passed directly to the write method of + the statement line. + {'partner_id': value, + 'account_id' : value, + + ...} + """ + res = {} + st_obj = self.pool.get('account.bank.statement.line') + st_line = st_obj.browse(cr,uid,line_id) + if st_line: + sql = "SELECT id FROM res_partner WHERE name ~* '.*%s.*'" + cr.execute(sql, (st_line.label,)) + result = cr.fetchall() + if len(result) > 1: + raise ErrorTooManyPartner(_('Line named "%s" was matched by more than one partner.')%(st_line.name,st_line.id)) + for id in result: + res['partner_id'] = id + st_vals = st_obj.get_values_for_line(cr, uid, profile_id = st_line.statement_id.profile_id.id, + partner_id = res.get('partner_id',False), line_type = st_line.type, st_line.amount, context) + res.update(st_vals) + return res class AccountStatementLine(Model): @@ -114,77 +178,100 @@ class AccountStatementLine(Model): } - - - def auto_complete_line(self, cr, uid, line, context=None): + def get_line_values_from_rules(self, cr, uid, ids, context=None): + """ + We'll try to find out the values related to the line based on what we + have and rules setted on the profile.. + """ + profile_obj = self.pool.get('account.statement.profil') res={} - if not line.partner_id or line.account_id.id ==1: - partner_obj = self.pool.get('res.partner') - partner_id=False - if line.order_ref: - partner_id = partner_obj.get_partner_from_order_ref(cr, uid, line.order_ref, context=context) - if not partner_id and line.email_address: - partner_id = partner_obj.get_partner_from_email(cr, uid, line.email_address, context=context) - if not partner_id and line.partner_name: - partner_id = partner_obj.get_partner_from_name(cr, uid, line.partner_name, context=context) - if not partner_id and line.label: - partner_id = partner_obj.get_partner_from_label_based_on_bank_statement_label(cr, uid, line.label, context=context) - if partner_id: - res = {'partner_id': partner_id} - if context['auto_completion']: - #Build the space for expr - space = { - 'self':self, - 'cr':cr, - 'uid':uid, - 'line': line, - 'res': res, - 'context':context, - } - exec context['auto_completion'] in space - if space.get('result', False): - res.update(space['result']) + errors_stack = [] + for line in self.browse(cr,uid, ids, context): + try: + vals = profile_obj.find_values_from_rules(cr, uid, ids, line.id, context) + res[line.id]=vals + except ErrorTooManyPartner, exc: + msg = "Line ID %s had following error: %s" % (line.id, str(exc)) + errors_stack.append(msg) + # if not auto_complete_line + # if not line.partner_id or line.account_id.id ==1: + # partner_obj = self.pool.get('res.partner') + # partner_id=False + # if line.order_ref: + # partner_id = partner_obj.get_partner_from_order_ref(cr, uid, line.order_ref, context=context) + # if not partner_id and line.email_address: + # partner_id = partner_obj.get_partner_from_email(cr, uid, line.email_address, context=context) + # if not partner_id and line.partner_name: + # partner_id = partner_obj.get_partner_from_name(cr, uid, line.partner_name, context=context) + # if not partner_id and line.label: + # partner_id = partner_obj.get_partner_from_label_based_on_bank_statement_label(cr, uid, line.label, context=context) + # if partner_id: + # res = {'partner_id': partner_id} + # if context['auto_completion']: + # #Build the space for expr + # space = { + # 'self':self, + # 'cr':cr, + # 'uid':uid, + # 'line': line, + # 'res': res, + # 'context':context, + # } + # exec context['auto_completion'] in space + # if space.get('result', False): + # res.update(space['result']) + if errors_stack: + msg = u"\n".join(errors_stack) + raise ErrorTooManyPartner(msg) return res - -class A(object): - def xx_toto(): - print 'toto' - -a = A() -funcs = ['yy_toto', 'xx_toto'] -for i in funcs: - if hasattr(a, i): - to_call = getattr(a, i) - to_call() - else: - raise NameError('blblblb') +# +# class A(object): +# def xx_toto(): +# print 'toto' +# +# +# a = A() +# funcs = ['yy_toto', 'xx_toto'] +# for i in funcs: +# if hasattr(a, i): +# to_call = getattr(a, i) +# to_call() +# else: +# raise NameError('blblblb') class AccountBankSatement(Model): - """We add a basic button and stuff to support the auto-completion - of the bank statement once line have been imported or manually entred. - """ + """ + We add a basic button and stuff to support the auto-completion + of the bank statement once line have been imported or manually entred. + """ _inherit = "account.bank.statement" - def button_auto_completion(self, cr, uid, ids, context=None): if not context: context={} stat_line_obj = self.pool.get('account.bank.statement.line') + errors_msg=False for stat in self.browse(cr, uid, ids, context=context): ctx = context.copy() - if stat.bank_statement_import_id: - ctx['partner_id'] = stat.bank_statement_import_id.partner_id.id - ctx['transferts_account_id'] = stat.bank_statement_import_id.transferts_account_id.id - ctx['credit_account_id'] = stat.bank_statement_import_id.credit_account_id.id - ctx['fee_account_id'] = stat.bank_statement_import_id.fee_account_id.id - ctx['auto_completion'] = stat.bank_statement_import_id.auto_completion - for line in stat.line_ids: - vals = stat_line_obj.auto_complete_line(cr, uid, line, context=ctx) - if not line.ref and not vals.get('ref', False): - vals['ref'] = stat.name + line_ids = map(lambda x:x.id, stat.line_ids) + try: + res = stat_line_obj.get_line_values_from_rules(cr, uid, line_ids, context=ctx) + except ErrorTooManyPartner, exc: + errors_msg = str(exc) + for id in line_ids: + vals = res[line.id] if vals: - stat_line_obj.write(cr, uid, line.id, vals, context=ctx) + stat_line_obj.write(cr, uid, id, vals, context=ctx) + # cr.commit() + # TOTEST: I don't know if this is working... + if errors_msg: + # raise osv.except_osv(_('Error'), errors_msg) + warning = { + 'title': _('Error!'), + 'message' : errors_msg, + } + return {'warning': warning} return True def auto_confirm(self, cr, uid, ids, context=None): diff --git a/account_statement_base_import/__init__.py b/account_statement_base_import/__init__.py index f6c46966..caaa01da 100644 --- a/account_statement_base_import/__init__.py +++ b/account_statement_base_import/__init__.py @@ -18,5 +18,6 @@ # along with this program. If not, see . # ############################################################################## - +import parser +import wizard import statement \ No newline at end of file diff --git a/account_statement_base_import/__openerp__.py b/account_statement_base_import/__openerp__.py index 535c33db..e99e8268 100644 --- a/account_statement_base_import/__openerp__.py +++ b/account_statement_base_import/__openerp__.py @@ -19,7 +19,7 @@ # ############################################################################## -{'name': "Bank statement easy import", +{'name': "Bank statement base import", 'version': '1.0', 'author': 'Camptocamp', 'maintainer': 'Camptocamp', @@ -27,9 +27,30 @@ 'complexity': 'normal', #easy, normal, expert 'depends': ['account_statement_ext','account_statement_base_completion'], 'description': """ - The goal of this module is bring basic method and fields on bank statement to deal with + This module bring basic methods and fields on bank statement to deal with the importation of different bank and offices. + A generic abstract method is defined and an example that provide a basic way of importing + bank statement through a standard .csv or .xml file is providen. + + The goal is here to populate the statement lines of a bank statement with the infos that the + bank or offic give you. Then, if you need to complete data from there, add your own + statement_*_completion module and implement the needed rules. + + This module improves the bank statement and allow you to import your bank transactions with + a standard .csv or .xls file (you'll find it in the 'datas' folder). It'll respect the profil + you'll choose (providen by the accouhnt_statement_ext module) to pass the entries. + + This module can handle a commission taken by the payment office and has the following format: + + * ref : the SO number, INV number or any matching ref found. It'll be used as reference + in the generated entries and will be useful for reconciliation process + * date : date of the payment + * amount : amount paid in the currency of the journal used in the importation profil + * commission_amount : amount of the comission for each line + * label : the comunication given by the payment office, used as communication in the + generated entries. + """, 'website': 'http://www.camptocamp.com', diff --git a/account_statement_base_import/datas/statement.csv b/account_statement_base_import/datas/statement.csv new file mode 100644 index 00000000..31e2b398 --- /dev/null +++ b/account_statement_base_import/datas/statement.csv @@ -0,0 +1,4 @@ +"ref";"date";"amount";"commission_amount";"label" +50969286;2011-03-07 13:45:14;118.4;-11.84;"label a" +51065326;2011-03-05 13:45:14;189;-15.12;"label b" +51179306;2011-03-02 17:45:14;189;-15.12;"label c" diff --git a/account_statement_base_import/datas/statement.xls b/account_statement_base_import/datas/statement.xls new file mode 100644 index 00000000..53ba58a5 Binary files /dev/null and b/account_statement_base_import/datas/statement.xls differ diff --git a/account_statement_base_import/parser/__init__.py b/account_statement_base_import/parser/__init__.py new file mode 100644 index 00000000..262ce699 --- /dev/null +++ b/account_statement_base_import/parser/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Nicolas Bessi +# Copyright 2011-2012 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from parser import new_bank_statement_parser +from parser import BankStatementImportParser +import file_parser +import generic_file_parser \ No newline at end of file diff --git a/account_statement_base_import/parser/file_parser.py b/account_statement_base_import/parser/file_parser.py new file mode 100644 index 00000000..69c4f0e2 --- /dev/null +++ b/account_statement_base_import/parser/file_parser.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright Camptocamp SA +# Author Nicolas Bessi, Joel Grand-Guillaume +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.tools.translate import _ +import base64 +import csv +import tempfile +import datetime +from . import parser +try: + import xlrd +except: + raise Exception(_('Please install python lib xlrd')) + +class FileParser(BankStatementImportParser): + """Abstract clall for that help to build a specific parser for all + .csv and .xls files""" + + def __init__(self, parse_name=None, keys_to_validate={}, ftype='csv', convertion_dict=None, *args, **kwargs): + """ + :param: convertion_dict : keys and type to convert of every column in the file like + { + 'ref': unicode, + 'label': unicode, + 'date': datetime.datetime, + 'amount': float, + 'commission_amount': float + } + + """ + + super(self,FileParser).__init__(parse_name, *args, **kwargs) + if ftype in ('csv', 'xls'): + self.ftype = ftype + else: + raise Exception(_('Invalide file type %s. please use csv or xls') % (ftype)) + self.keys_to_validate = keys_to_validate + self.convertion_dict = convertion_dict + + def _custom_format(self, *args, **kwargs): + return True + + def _pre(self, *args, **kwargs): + return True + + def _validate(self, *args, **kwargs): + parsed_cols = self.result_row_list[0].keys() + for col in self.keys_to_validate: + if col not in parsed_cols: + raise Exception(_('Column %s not present in file') % (col)) + return True + + def _post(self, *args, **kwargs): + """Cast row type depending on the file format .csv or .xls""" + self.result_row_list = self._cast_rows(kwargs) + return True + + def _parse(self, *args, **kwargs): + """Launch the parsing through .csv or .xls depending on the + given ftype""" + + res = None + if self.ftype == 'csv': + res = self._parse_csv() + else: + res = self._parse_xls() + self.result_row_list = res + return True + + def _parse_csv(self, delimiter=';'): + "return an array of dict from csv file" + csv_file = tempfile.NamedTemporaryFile() + csv_file.write(self.filebuffer) + # We ensure that cursor is at beginig of file + csv_file.seek(0) + reader = UnicodeDictReader( + open(csv_file.name).readlines(), + delimiter=delimiter + ) + return [x for x in reader] + + def _parse_xls(self): + "return an array of dict from xls file" + wb_file = tempfile.NamedTemporaryFile() + wb_file.write(self.filebuffer) + # We ensure that cursor is at beginig of file + wb_file.seek(0) + wb = xlrd.open_workbook(wb_file.name) + sheet = wb.sheet_by_index(0) + header = sheet.row_values(0) + res = [] + for rownum in range(1, sheet.nrows): + res.append(dict(zip(header, sheet.row_values(rownum)))) + try: + wb_file.close() + except Exception, e: + pass #file is allready closed + return res + + def _from_csv(self, result_set, conversion_rules): + for line in result_set: + for rule in conversion_rules: + if conversion_rules[rule] == datetime.datetime: + date_string = line[rule].split(' ')[0] + line[rule] = datetime.datetime.strptime(date_string, + '%Y-%m-%d') + else: + line[rule] = conversion_rules[rule](line[rule]) + return result_set + + def _from_xls(self, result_set, conversion_rules): + for line in result_set: + for rule in conversion_rules: + if conversion_rules[rule] == datetime.datetime: + t_tuple = xlrd.xldate_as_tuple(line[rule], 1) + line[rule] = datetime.datetime(*t_tuple) + else: + line[rule] = conversion_rules[rule](line[rule]) + return result_set + + def _cast_rows(self): + func = getattr(self, '_from_%s'%(self.ftype)) + res = func(self.result_row_list, self.convertion_dict) + return res diff --git a/account_statement_base_import/parser/generic_file_parser.py b/account_statement_base_import/parser/generic_file_parser.py new file mode 100644 index 00000000..92cf0559 --- /dev/null +++ b/account_statement_base_import/parser/generic_file_parser.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright Camptocamp SA +# Author Joel Grand-Guillaume +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.tools.translate import _ +import base64 +import csv +import tempfile +import datetime +from . import file_parser +try: + import xlrd +except: + raise Exception(_('Please install python lib xlrd')) + +class GenericFileParser(FileParser): + """Generic parser that use a define format in csv or xls to import + bank statement. This is mostely an example of how to proceed to create a new + parser, but will also be useful as it allow to import a basic flat file.""" + + + + def __init__(self, parse_name = None, ftype='csv'): + convertion_dict = { + 'ref': unicode, + 'label': unicode, + 'date': datetime.datetime, + 'amount': float, + 'commission_amount': float + } + # Order of cols does not matter but first row of the file has to be header + keys_to_validate = ['ref', 'label', 'date', 'amount', 'commission_amount'] + + + super(self,GenericFileParser).__init__(parser_for = parse_name, keys_to_validate={}, ftype='csv', convertion_dict=None ): + + @classmethod + def parser_for(cls, parser_name): + return parser_name == 'generic_csvxls_so' + + + + + + diff --git a/account_statement_base_import/parser/parser.py b/account_statement_base_import/parser/parser.py new file mode 100644 index 00000000..dcc98876 --- /dev/null +++ b/account_statement_base_import/parser/parser.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Joel Grand-Guillaume +# Copyright 2011-2012 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import base64 + + +def UnicodeDictReader(utf8_data, **kwargs): + csv_reader = csv.DictReader(utf8_data, **kwargs) + for row in csv_reader: + yield dict([(key, unicode(value, 'utf-8')) for key, value in row.iteritems()]) + +class BankStatementImportParser(object): + """Abstract class for defining parser for different files and + format to import in a bank statement""" + + + def __init__(self, parser_name = None, *args, **kwargs): + # The name of the parser as it will be called + self.parser_name = parser_name + # The result as a list of row + self.result_row_list = None + # The file buffer on which to work on + self.filebuffer = None + # Concatenate here the global commission taken by the bank/office + # for this statement. + self.commission_global_amount = None + + @classmethod + def parser_for(cls, parser_name): + return False + + def _decode_64b_stream(self): + self.filebuffer = base64.b64decode(self.filebuffer) + return True + + def _format(self, decode_base_64=True, **kwargs): + if decode_base_64: + self._decode_64b_stream() + self._custom_format(kwargs) + return True + + def _custom_format(self, *args, **kwargs): + """Implement a method to convert format, encoding and so on before + starting to work on datas.""" + return NotImplementedError + + + def _pre(self, *args, **kwargs): + """Implement a method to make a pre-treatment on datas before parsing + them, like concatenate stuff, and so...""" + return NotImplementedError + + def _validate(self, *args, **kwargs): + """Implement a method to validate the self.result_row_list instance + property and raise an error if not valid.""" + return NotImplementedError + + def _post(self, *args, **kwargs): + """Implement a method to make some last changes on the result of parsing + the datas, like converting dates, computing commission, ... """ + return NotImplementedError + + def _parse(self, *args, **kwargs): + """Implement a method to save the result of parsing self.filebuffer + in self.result_row_list instance property. Put the commission global + amount in the self.commission_global_amount one.""" + return NotImplementedError + + def parse(self, filebuffer, *args, **kwargs): + """This will be the method that will be called by wizard, button and so + to parse a filebuffer by calling successively all the private method + that need to be define for each parser. + Return: + [] of rows as {'key':value} + + Note: The row_list must contain only value that are present in the account. + bank.statement.line object !!! + """ + if filebuffer: + self.filebuffer = filebuffer + else: + raise Exception(_('No buffer file given.')) + self._format(args, kwargs) + self._pre(args, kwargs) + self._parse(args, kwargs) + self._validate(args, kwargs) + self._post(args, kwargs) + return self.result_row_list, + +def itersubclasses(cls, _seen=None): + """ + itersubclasses(cls) + + Generator over all subclasses of a given class, in depth first order. + + >>> list(itersubclasses(int)) == [bool] + True + >>> class A(object): pass + >>> class B(A): pass + >>> class C(A): pass + >>> class D(B,C): pass + >>> class E(D): pass + >>> + >>> for cls in itersubclasses(A): + ... print(cls.__name__) + B + D + E + C + >>> # get ALL (new-style) classes currently defined + >>> [cls.__name__ for cls in itersubclasses(object)] #doctest: +ELLIPSIS + ['type', ...'tuple', ...] + """ + if not isinstance(cls, type): + raise TypeError('itersubclasses must be called with ' + 'new-style classes, not %.100r' % cls) + if _seen is None: _seen = set() + try: + subs = cls.__subclasses__() + except TypeError: # fails only when cls is type + subs = cls.__subclasses__(cls) + for sub in subs: + if sub not in _seen: + _seen.add(sub) + yield sub + for sub in itersubclasses(sub, _seen): + yield sub + +def new_bank_statement_parser(parser_name, *args, **kwargs): + for cls in itersubclasses(BankStatementImportParser): + if cls.parser_for(parser_name): + return cls(parser_name, *args, **kwargs) + raise ValueError + \ No newline at end of file diff --git a/account_statement_base_import/statement.py b/account_statement_base_import/statement.py index ebb6f11c..50dd1a9a 100644 --- a/account_statement_base_import/statement.py +++ b/account_statement_base_import/statement.py @@ -24,58 +24,161 @@ import datetime import netsvc logger = netsvc.Logger() from openerp.osv.orm import Model, fields - +# from account_statement_base_import.parser.file_parser import FileParser +from parser import new_bank_statement_parser class AccountStatementProfil(Model): _inherit = "account.statement.profil" + + def get_type_selection(self, cr, uid, context=None): + """ + Has to be inherited to add parser + """ + return [('generic_csvxls_so', 'Generic .csv/.xls based on SO Name')] + + _columns = { 'launch_import_completion': fields.boolean("Launch completion after import", help="Tic that box to automatically launch the completion on each imported\ file using this profile."), - 'last_import_date': fields.date("Last Import Date"), + 'last_import_date': fields.datetime("Last Import Date"), 'rec_log': fields.text('log', readonly=True), + 'import_type': fields.selection(get_import_type_selection, 'Type of import', required=True, + help = "Choose here the method by which you want to import bank statement for this profil."), } - def launch_import_bank_statement(self, cr, uid, ids, context=None): - stat_obj = self.pool.get('account.bank.statement') + def write_logs_after_import(self, cr, uid, ids, statement_id, num_lines, context): for id in ids: - logger = netsvc.Logger() - res = self.action_import_bank_statement(cr, uid, id, conteaccount_xt) - #autocomplete bank statement - stat_obj.button_auto_completion(cr, uid, res['crids'], context=context) - stat_obj.auto_confirm(cr, uid, res['crids'], context=context) log = self.read(cr, uid, id, ['rec_log'], context=context)['rec_log'] log_line = log and log.split("\n") or [] - log_line[0:0] = [datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ' : ' + str(len(res['crids'])) + _(' bank statement have been imported and ' + str(len(res['exist_ids'])) + _(' bank statement already exist'))] + import_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + log_line[0:0] = [import_date + ' : ' + + _("Bank Statement ID %s have been imported with %s lines ") %(statement_id, num_lines)] log = "\n".join(log_line) - self.write(cr, uid, id, {'rec_log' : log}, context=context) - logger.notifyChannel('banck statement import', netsvc.LOG_INFO, "%s bank statement have been imported and %s bank statement already exist"%(len(res['crids']), len(res['exist_ids']))) + self.write(cr, uid, id, {'rec_log' : log, 'last_import_date':import_date}, context=context) + logger.notifyChannel('Bank Statement Import', netsvc.LOG_INFO, + "Bank Statement ID %s have been imported with %s lines "%(statement_id, num_lines)) return True - - def action_import_bank_statement(self, cr, uid, id, context=None): - '''not implemented in this module''' - return {} - def open_bank_statement(self, cr, uid, ids, context): - task = self.browse(cr, uid, ids, context=context)[0] + def statement_import(self, cursor, uid, ids, profile_id, file_stream, ftype="csv", context=None): + """Create a bank statement with the given profile and parser. It will fullfill the bank statement + with the values of the file providen, but will not complete data (like finding the partner, or + the right account). This will be done in a second step with the completion rules. + """ + context = context or {} + statement_obj = self.pool.get('account.bank.statement') + statement_line_obj = self.pool.get('account.bank.statement.line') + attachment_obj = self.pool.get('ir.attachment') + prof_obj = self.pool.get("account.statement.profil") + if not profile_id: + raise osv.except_osv( + _("No Profile !"), + _("You must provide a valid profile to import a bank statement !")) + else: + prof = prof_obj.browse(cursor,uid,profile_id,context) + partner_id = prof.partner_id and prof.partner_id.id or False + commission_account_id = prof.commission_account_id and prof.commission_account_id.id or False + commission_analytic_id = prof.commission_analytic_id and prof.commission_analytic_id.id or False - return { - 'name': 'Bank ', - 'view_type': 'form', - 'view_mode': 'form', - 'view_id': [252], - 'res_model': self._name, - 'type': 'ir.actions.act_window', - 'nodestroy': True, - 'target': 'current', - 'res_id': self.read(cr, uid, ids, ['bank_statement_ids'],context=context)[0]['bank_statement_ids'], - } + parser = new_bank_statement_parser(parse_name=prof.import_type, ftype=ftype) + result_row_list = parser.parse(file_stream) + + # Check all key are present in account.bank.statement.line !! + parsed_cols = self.result_row_list[0].keys() + for col in parsed_cols: + if col not in statement_line_obj.__columns__: + raise osv.except_osv( + _("Missing column !"), + _("Column %s you try to import is not present in the bank statement line !") %(col)) + + statement_id = statement_obj.create(cursor,uid,{'profile_id':prof.id,},context) + account_receivable, account_payable = self.get_default_pay_receiv_accounts(cursor, uid, context) + commission_global_amount = 0.0 + try: + # Record every line in the bank statement and compute the global commission + # based on the commission_amount column + for line in result_row_list: + line_partner_id = False + line_to_reconcile = False -class AccountBankSatement(Model): + commission_global_amount += line.get('commission_amount', 0.0) + values = { + 'name': line.get('label', line.get('ref','/')), + 'date': line.get('date', datetime.datetime.now().date()), + 'amount': line['amount'], + 'ref': line['ref'], + 'type': 'customer', + 'statement_id': statement_id, + #'account_id': journal.default_debit_account_id + } + values['account_id'] = self.get_account_for_counterpart( + cursor, + uid, + line['amount'], + account_receivable, + account_payable + ) + # we finally create the line in system + statement_line_obj.create(cursor, uid, values, context=context) - _inherit = "account.bank.statement" - - + # we create commission line + if commission_global_amount: + comm_values = { + 'name': 'IN '+ _('Commission line'), + 'date': datetime.datetime.now().date(), + 'amount': commission_global_amount, + 'partner_id': partner_id, + 'type': 'general', + 'statement_id': statement_id, + 'account_id': commission_account_id, + 'ref': 'commission', + 'analytic_account_id': commission_analytic_id + } + statement_line_obj.create(cursor, uid, + comm_values, + context=context) + attachment_obj.create( + cursor, + uid, + { + 'name': 'statement file', + 'datas': file_stream, + 'datas_fname': "%s.%s"%(datetime.datetime.now().date(), + ftype), + 'res_model': 'account.bank.statement', + 'res_id': statement_id, + }, + context=context + ) + # If user ask to launch completion at end of import, do it ! + if prof.launch_import_completion: + self.button_auto_completion(cursor, uid, statement_id, context) + + # Write the needed log infos on profile + self.write_logs_after_import(self, cr, uid, prof.id, statement_id, + len(result_row_list), context) + + except Exception, exc: + logger.notifyChannel("Statement import", + netsvc.LOG_ERROR, + _("Statement can not be created %s") %(exc,)) + + statement_obj.unlink(cursor, uid, [statement_id]) + raise exc + return statement_id + + + +class AccountStatementLine(Model): + """Add sparse field on the statement line to allow to store all the + bank infos that are given by an office.""" + _inherit = "account.bank.statement.line" + + _columns={ + 'commission_amount': fields.sparse(type='float', string='Line Commission Amount', + serialization_field='additionnal_bank_fields'), + + } diff --git a/account_statement_base_import/statement_view.xml b/account_statement_base_import/statement_view.xml index 508aeb9c..159e53b8 100644 --- a/account_statement_base_import/statement_view.xml +++ b/account_statement_base_import/statement_view.xml @@ -13,35 +13,31 @@ -