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 @@
-
+
+
-
+
+ account_bank_statement_import_base.bank_statement.view_form
+ account.bank.statement
+
+ form
+
+
+
+
+
+
+
+
-
diff --git a/account_statement_base_import/wizard/__init__.py b/account_statement_base_import/wizard/__init__.py
new file mode 100644
index 00000000..5dbd2b0c
--- /dev/null
+++ b/account_statement_base_import/wizard/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author Nicolas Bessi, Joel Grand-Guillaume. Copyright Camptocamp SA
+#
+# 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 .
+#
+##############################################################################
+import import_statement
diff --git a/account_statement_base_import/wizard/import_statement.py b/account_statement_base_import/wizard/import_statement.py
new file mode 100644
index 00000000..20f58b89
--- /dev/null
+++ b/account_statement_base_import/wizard/import_statement.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi, 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 .
+#
+##############################################################################
+
+"""
+Wizard to import financial institute date in bank statement
+"""
+
+from osv import fields, osv
+from tools.translate import _
+import os
+
+class CreditPartnerStatementImporter(osv.osv_memory):
+ """Import Credit statement"""
+
+ _name = "credit.statement.import"
+
+ def _get_profile(self, cr, uid, context=None):
+ if context is None: context = {}
+ res = False
+ if (context.get('active_model', False) == 'account.statement.profil' and
+ context.get('active_ids', False)):
+ res = context['active_ids']
+ if len(res) > 1:
+ raise Exception (_('You cannot use this on more than one profile !'))
+ return res[0]
+
+ _columns = {
+
+ 'profile_id': fields.many2one('account.statement.profil',
+ 'Import configuration parameter',
+ required=True),
+ 'input_statement': fields.binary('Statement file', required=True),
+ 'partner_id': fields.many2one('res.partner',
+ 'Credit insitute partner',
+ ),
+ 'journal_id': fields.many2one('account.journal',
+ 'Financial journal to use transaction',
+ ),
+ 'input_statement': fields.binary('Statement file', required=True),
+ 'file_name': fields.char('File Name', size=128),
+ 'commission_account_id': fields.many2one('account.account',
+ 'Commission account',
+ ),
+ 'commission_analytic_id': fields.many2one('account.analytic.account',
+ 'Commission analytic account',
+ ),
+ 'receivable_account_id': fields.many2one('account.account',
+ 'Force Receivable/Payable Account'),
+ 'force_partner_on_bank': fields.boolean('Force partner on bank move',
+ help="Tic that box if you want to use the credit insitute partner\
+ in the counterpart of the treasury/banking move."
+ ),
+ 'balance_check': fields.boolean('Balance check',
+ help="Tic that box if you want OpenERP to control the start/end balance\
+ before confirming a bank statement. If don't ticked, no balance control will be done."
+ ),
+
+ }
+
+ _defaults = {
+ 'profile_id': _get_profile,
+ }
+
+ def onchange_profile_id(self, cr, uid, ids, profile_id, context=None):
+ res={}
+ if profile_id:
+ c = self.pool.get("account.statement.profil").browse(cr,uid,profile_id)
+ res = {'value': {'partner_id': c.partner_id and c.partner_id.id or False,
+ 'journal_id': c.journal_id and c.journal_id.id or False, 'commission_account_id': \
+ c.commission_account_id and c.commission_account_id.id or False,
+ 'receivable_account_id': c.receivable_account_id and c.receivable_account_id.id or False,
+ 'commission_a':c.commission_analytic_id and c.commission_analytic_id.id or False,
+ 'force_partner_on_bank':c.force_partner_on_bank,
+ 'balance_check':c.balance_check,}}
+ return res
+
+ def _check_extension(self, filename):
+ (shortname, ftype) = os.path.splitext(file_name)
+ if not ftype:
+ #We do not use osv exception we do not want to have it logged
+ raise Exception(_('Please use a file with an extention'))
+ return ftype
+
+ def import_statement(self, cursor, uid, req_id, context=None):
+ """This Function import credit card agency statement"""
+ context = context or {}
+ if isinstance(req_id, list):
+ req_id = req_id[0]
+ importer = self.browse(cursor, uid, req_id, context)
+ ftype = self._check_extension(importer.file_name)
+ sid = self.pool.get(
+ 'account.bank.statement').statement_import(
+ cursor,
+ uid,
+ False,
+ importer.profile_id.id,
+ importer.input_statement,
+ ftype.replace('.',''),
+ context=context
+ )
+ # obj_data = self.pool.get('ir.model.data')
+ # act_obj = self.pool.get('ir.actions.act_window')
+ # result = obj_data.get_object_reference(cursor, uid, 'account_statement_import', 'action_treasury_statement_tree')
+ #
+ # id = result and result[1] or False
+ # result = act_obj.read(cursor, uid, [id], context=context)[0]
+ # result['domain'] = str([('id','in',[sid])])
+
+ # We should return here the profile for which we executed the import
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/account_statement_base_import/wizard/import_statement_view.xml b/account_statement_base_import/wizard/import_statement_view.xml
new file mode 100644
index 00000000..10b0b47d
--- /dev/null
+++ b/account_statement_base_import/wizard/import_statement_view.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ credit.statement.import.config.view
+ credit.statement.import
+ form
+
+
+
+
+
+
+ Import statement
+ credit.statement.import
+ form
+ tree,form
+
+ new
+
+
+
+
+
+
diff --git a/account_statement_ext/__init__.py b/account_statement_ext/__init__.py
index 701622aa..b809b3ad 100644
--- a/account_statement_ext/__init__.py
+++ b/account_statement_ext/__init__.py
@@ -19,8 +19,6 @@
#
##############################################################################
-import file_parser
-import wizard
import statement
import report
import account
\ No newline at end of file
diff --git a/account_statement_ext/__openerp__.py b/account_statement_ext/__openerp__.py
index 98cd29dc..d25750bb 100644
--- a/account_statement_ext/__openerp__.py
+++ b/account_statement_ext/__openerp__.py
@@ -25,7 +25,7 @@
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
- 'depends': ['base_transaction_id'],
+ 'depends': ['account'],
'description': """
The goal of this module is to improve the basic bank statement, help dealing with huge volume of
reconciliation through payment offices like Paypal, Lazer, Visa, Amazon and so on.
diff --git a/account_statement_ext/account.py b/account_statement_ext/account.py
index 01886734..b72a80c9 100644
--- a/account_statement_ext/account.py
+++ b/account_statement_ext/account.py
@@ -22,7 +22,7 @@ import netsvc
logger = netsvc.Logger()
from openerp.osv.orm import Model, fields
-class account_move(osv.osv):
+class account_move(Model):
_inherit='account.move'
def unlink(self, cr, uid, ids, context=None):
@@ -33,6 +33,6 @@ class account_move(osv.osv):
if move_line.reconcile_id:
move_line.reconcile_id.unlink(context=context)
return super(account_move, self).unlink(cr, uid, ids, context=context)
-account_move()
+
diff --git a/account_statement_ext/statement.py b/account_statement_ext/statement.py
index 87159a17..a72319db 100644
--- a/account_statement_ext/statement.py
+++ b/account_statement_ext/statement.py
@@ -20,7 +20,6 @@
##############################################################################
from tools.translate import _
-from account_statement_ext.file_parser.parser import FileParser
import datetime
import netsvc
logger = netsvc.Logger()
@@ -395,43 +394,38 @@ class AccountBankSatement(Model):
)
return account_id
- def get_default_pay_receiv_accounts(self, cursor, uid, receivable_account_id, context=None):
+ def get_default_pay_receiv_accounts(self, cursor, uid, context=None):
"""We try to determine default payable/receivable accounts to be used as counterpart
- of the journal one.
-
- If receivable_account_id is set (from the profil), take it as receivable/payable
- account. Otherwise take it from the property of the partner."""
+ from the company default propoerty.
+ """
account_receivable = False
account_payable = False
- if receivable_account_id:
- account_receivable = account_payable = receivable_account_id
- else:
- context = context or {}
- property_obj = self.pool.get('ir.property')
- model_fields_obj = self.pool.get('ir.model.fields')
- model_fields_ids = model_fields_obj.search(
- cursor,
- uid,
- [('name', 'in', ['property_account_receivable',
- 'property_account_payable']),
- ('model', '=', 'res.partner'),],
- context=context
- )
- property_ids = property_obj.search(
- cursor,
- uid, [
- ('fields_id', 'in', model_fields_ids),
- ('res_id', '=', False),
- ],
- context=context
- )
-
- for erp_property in property_obj.browse(cursor, uid,
- property_ids, context=context):
- if erp_property.fields_id.name == 'property_account_receivable':
- account_receivable = erp_property.value_reference.id
- elif erp_property.fields_id.name == 'property_account_payable':
- account_payable = erp_property.value_reference.id
+ context = context or {}
+ property_obj = self.pool.get('ir.property')
+ model_fields_obj = self.pool.get('ir.model.fields')
+ model_fields_ids = model_fields_obj.search(
+ cursor,
+ uid,
+ [('name', 'in', ['property_account_receivable',
+ 'property_account_payable']),
+ ('model', '=', 'res.partner'),],
+ context=context
+ )
+ property_ids = property_obj.search(
+ cursor,
+ uid, [
+ ('fields_id', 'in', model_fields_ids),
+ ('res_id', '=', False),
+ ],
+ context=context
+ )
+
+ for erp_property in property_obj.browse(cursor, uid,
+ property_ids, context=context):
+ if erp_property.fields_id.name == 'property_account_receivable':
+ account_receivable = erp_property.value_reference.id
+ elif erp_property.fields_id.name == 'property_account_payable':
+ account_payable = erp_property.value_reference.id
return account_receivable, account_payable
def balance_check(self, cr, uid, st_id, journal_type='bank', context=None):
@@ -465,19 +459,78 @@ class AccountBankSatementLine(Model):
return periods and periods[0] or False
_columns = {
- # Set them as required
- 'ref': fields.char('Reference', size=32, required=True),
+ # Set them as required + 64 char instead of 32
+ 'ref': fields.char('Reference', size=64, required=True),
'period_id': fields.many2one('account.period', 'Period', required=True),
}
_defaults = {
'period_id': _get_period,
}
-
-
+ def get_values_for_line(self, cr, uid, profile_id = False, partner_id = False, line_type = False, amount = False, context = None):
+ """Return the account_id to be used in the line of a bank statement. It'll base the result as follow:
+ - If a receivable_account_id is set in the profil, return this value and type = general
+ - Elif line_type is given, take the partner receivable/payable property (payable if type= supplier, receivable
+ otherwise)
+ - Elif amount is given, take the partner receivable/payable property (receivable if amount >= 0.0,
+ payable otherwise). In that case, we also fullfill the type (receivable = customer, payable = supplier)
+ so it is easier for the accountant to know why the receivable/payable has been chosen
+ - Then, if no partner are given we look and take the property from the company so we always give a value
+ for account_id. Note that in that case, we return the receivable one.
+ :params: profile_id: int/long
+ :params: line_type: String value like 'general', 'supplier', 'customer'
+ :params: amount: float
+ :return A dict of value that can be passed directly to the write method of
+ the statement line:
+ {'partner_id': value,
+ 'account_id' : value,
+ 'type' : value,
+ ...
+ }
+ """
+ if context is None:
+ context = {}
+ res = {}
+ obj_partner = self.pool.get('res.partner')
+ obj_stat = self.pool.get('account.bank.statement')
+ receiv_account, pay_account, account_id = False
+ # If profil has a receivable_account_id, we return it in any case
+ if profile_id:
+ profile = self.pool.get("account.statement.profil").browse(cr,uid,profile_id)
+ if profile.receivable_account_id:
+ res['account_id'] = profile.receivable_account_id.id
+ res['type'] = 'general'
+ return res
+ # If partner -> take from him
+ if partner_id:
+ part = obj_partner.browse(cr, uid, partner_id, context=context)
+ pay_account = part.property_account_payable.id
+ receiv_account = part.property_account_receivable.id
+ # If no value, look on the default company property
+ if not pay_account or not receiv_account:
+ receiv_account, pay_account = obj_stat.get_default_pay_receiv_accounts(cr, uid, context=None)
+ # Now we have both pay and receive account, choose the one to use
+ # based on line_type first, then amount, otherwise take receivable one.
+ if line_type is not False:
+ if line_type == 'supplier':
+ res['account_id'] = pay_account
+ else:
+ res['account_id'] = receiv_account
+ elif amount is not False:
+ if amount >= 0:
+ res['account_id'] = receiv_account
+ res['type'] = 'customer'
+ else:
+ res['account_id'] = pay_account
+ res['type'] = 'supplier'
+ if not account_id:
+ res['account_id'] = receiv_account
+ return res
+
+
def onchange_partner_id(self, cr, uid, ids, partner_id, profile_id, context=None):
- """When changing the partner, we'll now need to look in the profil to determine which
- account to use."""
+ """Override of the basic method as we need to pass the profile_id in the on_change_type
+ call."""
obj_partner = self.pool.get('res.partner')
if context is None:
context = {}
@@ -493,26 +546,21 @@ class AccountBankSatementLine(Model):
type = 'supplier'
if part.customer == True:
type = 'customer'
- res_type = self.onchange_type(cr, uid, ids, partner_id, type, profile_id, context=context)
+ res_type = self.onchange_type(cr, uid, ids, partner_id, type, profile_id, context=context) # Chg
if res_type['value'] and res_type['value'].get('account_id', False):
- res = {'value': {'type': type, 'account_id': res_type['value']['account_id']}}
- else:
- res = {'value': {'type': type}}
-
- c = self.pool.get("account.statement.profil").browse(cr,uid,profile_id)
- acc_id=c.receivable_account_id and c.receivable_account_id.id or False
- if acc_id:
- res['value'].update({'account_id':acc_id})
- return res
+ return {'value': {'type': type, 'account_id': res_type['value']['account_id']}}
+ return {'value': {'type': type}}
- # TOFIX: don't seems to work as expected
def onchange_type(self, cr, uid, line_id, partner_id, type, profile_id, context=None):
+ """Keep the same features as in standard and call super. If an account is returned,
+ call the method to compute line values."""
if context is None:
context = {}
res = super(AccountBankSatementLine,self).onchange_type(cr, uid, line_id, partner_id, type, context)
- c = self.pool.get("account.statement.profil").browse(cr,uid,profile_id)
- acc_id=c.receivable_account_id and c.receivable_account_id.id or False
- if acc_id:
- res['value'].update({'account_id':acc_id})
+ if 'account_id' in res['value']:
+ result = self.get_values_for_line(cr, uid, profile_id = profile_id,
+ partner_id = partner_id, line_type = type, context = context)
+ if result:
+ res['value'].update({'account_id':result['account_id']})
return res
diff --git a/account_statement_ext/statement_view.xml b/account_statement_ext/statement_view.xml
index eb5f94a7..c5b4f7c1 100644
--- a/account_statement_ext/statement_view.xml
+++ b/account_statement_ext/statement_view.xml
@@ -189,31 +189,13 @@
-
diff --git a/account_statement_transactionid_completion/__openerp__.py b/account_statement_transactionid_completion/__openerp__.py
index 79671cb1..caddd6bf 100644
--- a/account_statement_transactionid_completion/__openerp__.py
+++ b/account_statement_transactionid_completion/__openerp__.py
@@ -25,23 +25,28 @@
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
- 'depends': ['base_transaction_id','account_statement_base_completion'],
+ 'depends': ['account_statement_base_completion', 'base_transaction_id'],
'description': """
Add a completion method based on transaction ID providen by the bank/office. This
transaction ID has been recorded on the SO (by a mapping through the e-commerce connector,
- or manually). Completion will look in the SO with that transaction ID to match the partner.
+ or manually). Completion will look in the SO with that transaction ID to match the partner,
+ then it will complete the bank statement line with him the fullfill as well the reference
+ with the found SO name to ease the reconciliation.
+
+ So this way, the reconciliation always happend on the SO name stored in ref.
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': [
"statement_view.xml",
+ "data.xml",
],
'demo_xml': [],
'test': [],
'installable': True,
'images': [],
- 'auto_install': False,
+ 'auto_install': True,
'license': 'AGPL-3',
'active': False,
}
diff --git a/account_statement_transactionid_completion/data.xml b/account_statement_transactionid_completion/data.xml
new file mode 100644
index 00000000..23f366df
--- /dev/null
+++ b/account_statement_transactionid_completion/data.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Match from line reference (based on transaction ID)
+ 40
+ get_from_transaction_id_and_so
+
+
+
+
+
diff --git a/account_statement_transactionid_completion/statement.py b/account_statement_transactionid_completion/statement.py
index cea4fece..d0786ace 100644
--- a/account_statement_transactionid_completion/statement.py
+++ b/account_statement_transactionid_completion/statement.py
@@ -24,15 +24,51 @@ import datetime
import netsvc
logger = netsvc.Logger()
from openerp.osv.orm import Model, fields
+from openerp.addons.account_statement_base_completion.statement import ErrorTooManyPartner
-
-class AccountStatementProfil(Model):
- _inherit = "account.statement.profil"
+class AccountStatementCompletionRule(Model):
+ """Add a rule based on transaction ID"""
+ _inherit = "account.statement.completion.rule"
-class AccountBankSatement(Model):
+ def _get_functions(self):
+ res = super (self,AccountStatementCompletionRule)._get_functions()
+ res.append(('get_from_transaction_id_and_so', 'From line reference (based on SO transaction ID'))
+ return res
+
+ _columns={
+ 'function_to_call': fields.selection(_get_functions, 'Method'),
+ }
+
+ #TODO : Ensure we match only one partner => Otherwise raise an error !!!
+ def get_from_transaction_id_and_so(self, cr, uid, line_id, context=None):
+ """Match the partner based on the transaction ID field of the SO.
+ Then, call the generic st_line method to complete other values.
+ In that case, we always fullfill the reference of the line with the SO name.
+ Return:
+ A dict of value that can be passed directly to the write method of
+ the statement line.
+ {'partner_id': value,
+ 'account_id' : value,
+ ...}
+ """
+ st_obj = self.pool.get('account.bank.statement.line')
+ st_line = st_obj.browse(cr,uid,line_id)
+ res = {}
+ if st_line:
+ so_obj = self.pool.get('sale.order')
+ so_id = so_obj.search(cursor, uid, [('transaction_id', '=', st_line.transaction_id)])
+ if so_id and len(so_id) == 1:
+ so = so_obj.browse(cursor, uid, so_id[0])
+ res['partner_id'] = so.partner_id.id
+ res['ref'] = so.name
+ elif so_id and len(so_id) > 1:
+ raise Exception(_('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, st_line.amount, context)
+ res.update(st_vals)
+ return res
- _inherit = "account.bank.statement"
class AccountStatementLine(Model):
_inherit = "account.bank.statement.line"