diff --git a/account_statement_ext/__init__.py b/account_statement_ext/__init__.py index a8ce7c24..c421ad5b 100644 --- a/account_statement_ext/__init__.py +++ b/account_statement_ext/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- ############################################################################## # -# Author: Joel Grand-Guillaume +# Author: Nicolas Bessi, Joel Grand-Guillaume # Copyright 2011-2012 Camptocamp SA # # This program is free software: you can redistribute it and/or modify @@ -19,4 +19,7 @@ # ############################################################################## +import file_parser +import wizard import statement +import report diff --git a/account_statement_ext/__openerp__.py b/account_statement_ext/__openerp__.py index 7056f184..5d65ad90 100644 --- a/account_statement_ext/__openerp__.py +++ b/account_statement_ext/__openerp__.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## # -# Author: Joel Grand-Guillaume +# Author: Nicolas Bessi, Joel Grand-Guillaume # Copyright 2011-2012 Camptocamp SA -# Thanks to EduSense BV () for some part and idea -# taken from the account_banking module # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -21,21 +19,49 @@ # ############################################################################## -{'name': "Account Bank Statement without Period", +{'name': "Bank statement extension and profiles", 'version': '1.0', 'author': 'Camptocamp', 'maintainer': 'Camptocamp', 'category': 'Finance', 'complexity': 'normal', #easy, normal, expert - 'depends': ['account'], + 'depends': ['base_transaction_id'], 'description': """ - Remove the period on the bank statement, and compute it for each line based on their date instead. - If errors occurs, it will summarize them all in one popup instead of blocking all the process at every error. + The goal of this module is to help dealing with huge volume of reconciliation through + payment offices like Paypal, Lazer, Visa, Amazon and so on. It's mostly used for + E-commerce but can be usefule for other use cases as it introduce a notion of profil + on the bank statement to have more control on the generated entries. + + Features: + + 1) This module improves the bank statement that allow and you to import your bank transactions with + a standard .csv or .xls file (you'll find it in the 'data' folder). You can now define profile for each + Office or Bank that will generate the entries based on some criteria. You can setup: + + - Account commission and partner relation + - Can force an account for the reconciliation + - Choose to use balance check or not + - Analytic account for commission + - Force Partner on the counter-part move (e.g. 100.- debit, Partner: M.Martin; 100.- credit, Partner: HSBC) + + 2) Adds a report on bank statement that can be used for Checks + + 3) When an error occurs in a bank statement confirmation, it will go through all line anyway and summarize + all the erronous line in a same popup instead of raising and crashing on every step. + + 4) Remove the period on the bank statement, and compute it for each line based on their date instead. + + 5) Provide a standard import format to create and fullfill a bank statement from a .csv or .xls file. For + + """, 'website': 'http://www.camptocamp.com', 'init_xml': [], 'update_xml': [ 'statement_view.xml', + 'wizard/import_statement_view.xml', + 'report/bank_statement_webkit_header.xml', + 'report.xml', ], 'demo_xml': [], 'test': [], diff --git a/account_statement_ext/datas/statement.csv b/account_statement_ext/datas/statement.csv new file mode 100644 index 00000000..491df223 --- /dev/null +++ b/account_statement_ext/datas/statement.csv @@ -0,0 +1,4 @@ +"transaction_id";"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_ext/datas/statement.xls b/account_statement_ext/datas/statement.xls new file mode 100644 index 00000000..13eeafee Binary files /dev/null and b/account_statement_ext/datas/statement.xls differ diff --git a/account_statement_ext/file_parser/__init__.py b/account_statement_ext/file_parser/__init__.py new file mode 100644 index 00000000..b0665b1c --- /dev/null +++ b/account_statement_ext/file_parser/__init__.py @@ -0,0 +1,22 @@ +# -*- 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 . +# +############################################################################## + +import parser \ No newline at end of file diff --git a/account_statement_ext/file_parser/parser.py b/account_statement_ext/file_parser/parser.py new file mode 100644 index 00000000..9d11aa51 --- /dev/null +++ b/account_statement_ext/file_parser/parser.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright Camptocamp SA +# Author Nicolas Bessi +# 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 . +# +############################################################################## + +""" +Parser for csv or xml for file containing credit transfert data from +financial institure as VISA +""" + +from tools.translate import _ +import base64 +import csv +import tempfile +import datetime +try: + import xlrd +except: + raise Exception(_('Please install python lib xlrd')) + +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()]) + +# TODO extract into a lib helper module +class FileParser(object): + def __init__(self, filebuffer, keys_to_validate=None, decode_base_64=True, ftype='csv'): + 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.decode_base_64 = decode_base_64 + if filebuffer: + self.filebuffer = filebuffer + else: + raise Exception(_('No buffer file')) + + def parse(self): + "launch parsing of csv or xls" + if self.decode_base_64: + self._decode_64b_stream() + res = None + if self.ftype == 'csv': + res = self.parse_csv() + else: + res = self.parse_xls() + if self.keys_to_validate: + self._validate_column(res, self.keys_to_validate) + return res + + def _decode_64b_stream(self): + self.filebuffer = base64.b64decode(self.filebuffer) + return self.filebuffer + + def _validate_column(self, array_of_dict, cols): + parsed_cols = array_of_dict[0].keys() + for col in cols: + if col not in parsed_cols: + raise Exception(_('col %s not present in file') % (col)) + + 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 csv 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, result_set, conversion_rules): + func = getattr(self, '_from_%s'%(self.ftype)) + return func(result_set, conversion_rules) diff --git a/account_statement_ext/report.xml b/account_statement_ext/report.xml new file mode 100644 index 00000000..446735e4 --- /dev/null +++ b/account_statement_ext/report.xml @@ -0,0 +1,24 @@ + + + + + + + + Bank Statement + client_print_multi + + action + account.bank.statement + + + + diff --git a/account_statement_ext/report/__init__.py b/account_statement_ext/report/__init__.py new file mode 100644 index 00000000..0b2b7aee --- /dev/null +++ b/account_statement_ext/report/__init__.py @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- +# +# __init__.py +# +# Copyright (c) 2009 CamptoCamp. All rights reserved. +############################################################################## +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +import bank_statement_report \ No newline at end of file diff --git a/account_statement_ext/report/bank_statement_report.mako b/account_statement_ext/report/bank_statement_report.mako new file mode 100644 index 00000000..4feb28dd --- /dev/null +++ b/account_statement_ext/report/bank_statement_report.mako @@ -0,0 +1,69 @@ + + + + + + + <%! + def amount(text): + return text.replace('-', '‑') # replace by a non-breaking hyphen (it will not word-wrap between hyphen and numbers) + %> + + <%setLang(user.context_lang)%> + %for statement in objects: + +
+
+
${_('Bordereau')}
+
${_('Date')}
+
+
+
${ statement.name }
+
${ formatLang(statement.date,date=True) }
+
+
+ + +
+
+
+ ## date +
${_('Reference')}
+ ## period +
${_('Partenaire')}
+ ## move +
${_('Montant')}
+ ## journal +
+
+ <% sum_statement = 0.0 %> + %for statementline in statement.line_ids: +
+ ## curency code +
${statementline.name or '' }
+ ## currency balance +
${statementline.partner_id.name or '' }
+ ## currency balance cumulated +
${formatLang(statementline.amount) | amount }
+ <% sum_statement += statementline.amount %> +
+ %endfor +
+ ## curency code +
+ ## currency balance +
Total
+ ## currency balance cumulated +
${formatLang(sum_statement) }
+
+ +
+ %endfor + + + + + + diff --git a/account_statement_ext/report/bank_statement_report.py b/account_statement_ext/report/bank_statement_report.py new file mode 100644 index 00000000..a06c1d9a --- /dev/null +++ b/account_statement_ext/report/bank_statement_report.py @@ -0,0 +1,72 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Author: Nicolas Bessi. Copyright 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 time + +from report import report_sxw +from osv import osv +from tools.translate import _ +import pooler +from operator import add, itemgetter +from itertools import groupby +from datetime import datetime + +#from common_report_header_webkit import CommonReportHeaderWebkit +from report_webkit import webkit_report + +class BankStatementWebkit(report_sxw.rml_parse): + + def __init__(self, cursor, uid, name, context): + super(BankStatementWebkit, self).__init__(cursor, uid, name, context=context) + self.pool = pooler.get_pool(self.cr.dbname) + self.cursor = self.cr + + company = self.pool.get('res.users').browse(self.cr, uid, uid, context=context).company_id + header_report_name = ' - '.join((_('BORDEREAU DE REMISE DE CHEQUES'), + company.name, company.currency_id.name)) + statement = self.pool.get('account.bank.statement').browse(cursor,uid,context['active_id']); + footer_date_time = self.formatLang(str(datetime.today())[:19], date_time=True) + self.localcontext.update({ + 'cr': cursor, + 'uid': uid, + 'get_bank_statement' : self._get_bank_statement_data, + 'report_name': _('BORDEREAU DE REMISE DE CHEQUES'), + 'additional_args': [ + ('--header-font-name', 'Helvetica'), + ('--footer-font-name', 'Helvetica'), + ('--header-font-size', '10'), + ('--footer-font-size', '6'), + ('--header-left', header_report_name), + ('--header-spacing', '2'), + ('--footer-left', footer_date_time), + ('--footer-right', ' '.join((_('Page'), '[page]', _('of'), '[topage]'))), + ('--footer-line',), + ], + }) + def _get_bank_statement_data(self,statement): + statement_obj = self.pool.get('account.bank.statement.line') + statement_line_ids = statement_obj.search(self.cr,self.uid,[['statement_id','=',statement.id]]) + statement_lines = statement_obj.browse(self.cr,self.uid,statement_line_ids) + return statement_lines + +webkit_report.WebKitParser('report.report_bank_statement_webkit', + 'account.bank.statement', + 'addons/account_statement_import/report/bank_statement_report.mako', + parser=BankStatementWebkit) diff --git a/account_statement_ext/report/bank_statement_webkit_header.xml b/account_statement_ext/report/bank_statement_webkit_header.xml new file mode 100644 index 00000000..b837661f --- /dev/null +++ b/account_statement_ext/report/bank_statement_webkit_header.xml @@ -0,0 +1,180 @@ + + + + + + + + + + <% import datetime %> + + + + + + + + +
${formatLang( str(datetime.datetime.today()), date_time=True)}${user.name}Page  of 
+ +]]>
+ Portrait + A4 + + + + + + + + + + + +
${report_name} - ${company.partner_id.name | entity} - ${company.currency_id.name | entity}
${_debug or ''|n} +]]> +
+ + + + Bank Statement Landscape Header +
+
+
diff --git a/account_statement_ext/statement.py b/account_statement_ext/statement.py index 98e60936..5fba0b19 100644 --- a/account_statement_ext/statement.py +++ b/account_statement_ext/statement.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- ############################################################################## # -# Author: Joel Grand-Guillaume +# Author: Nicolas Bessi, Joel Grand-Guillaume # Copyright 2011-2012 Camptocamp SA # # This program is free software: you can redistribute it and/or modify @@ -19,28 +19,113 @@ # ############################################################################## -from osv import fields, osv from tools.translate import _ -from account_statement_import.file_parser.parser import FileParser +from account_statement_ext.file_parser.parser import FileParser import datetime import netsvc logger = netsvc.Logger() +from openerp.osv.orm import Model, fields + +class AccountStatementProfil(Model): + _name = "account.statement.profil" + _description = "Statement Profil" + + _columns = { + 'name': fields.char('Name', size=128, required=True), + 'partner_id': fields.many2one('res.partner', + 'Credit insitute partner', + help="Put a partner if you want to have it on the commission move (and optionaly\ + on the counterpart of the intermediate/banking move if you tic the corresponding checkbox)."), + 'journal_id': fields.many2one('account.journal', + 'Financial journal to use for transaction', + required=True), + 'commission_account_id': fields.many2one('account.account', + 'Commission account', + required=True), + 'commission_analytic_id': fields.many2one('account.analytic.account', + 'Commission analytic account'), + 'receivable_account_id': fields.many2one('account.account', + 'Force Receivable/Payable Account', + help="Choose a receivable account to force the default\ + debit/credit account (eg. an intermediat bank account instead of\ + default debitors)."), + '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 intermediat/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 = {} + + def _check_partner(self, cr, uid, ids, context=None): + obj = self.browse(cr, uid, ids[0], context=context) + if obj.partner_id == False and obj.force_partner_on_bank: + return False + return True + + _constraints = [ + (_check_partner, "You need to put a partner if you tic the 'Force partner on bank move' !", []), + ] -class AccountSatement(osv.osv): - """Override account bank statement to remove the period on it - and compute it for each line.""" +class AccountBankSatement(Model): + """A kind of bank statement for intermediate move between customer and real bank, used + for manageing check, payment office like paypal or marketplace like amazon. + We inherit account.bank.statement because it's a very close object with only some + difference. But we want some method to be completely different, so we create a new object.""" _inherit = "account.bank.statement" _columns = { + 'import_config_id': fields.many2one('account.statement.profil', + 'Profil', required=True, states={'draft': [('readonly', False)]}), + 'credit_partner_id': fields.related( + 'import_config_id', + 'partner_id', + type='many2one', + relation='res.partner', + string='Financial Partner', + store=True, readonly=True), + 'balance_check': fields.related( + 'import_config_id', + 'balance_check', + type='boolean', + string='Balance check', + store=True, readonly=True), + 'journal_id': fields.related( + 'import_config_id', + 'journal_id', + type='many2one', + relation='account.journal', + string='Journal', + store=True, readonly=True), + # 'line_ids': fields.one2many('account.bank.statement.line', + # 'statement_id', 'Statement lines', + # states={'confirm':[('readonly', True)]}), + # 'move_line_ids': fields.one2many('account.move.line', 'statement_treasury_id', + # 'Entry lines', states={'confirm':[('readonly',True)]}), + # Redefine this field to avoid his computation (it is a function field on bank statement) + # 'balance_end': fields.dummy(string="Computed Balance"), 'period_id': fields.many2one('account.period', 'Period', required=False, readonly=True), } _defaults = { 'period_id': lambda *a: False, } - + + def create(self, cr, uid, vals, context=None): + """Need to pass the journal_id in vals anytime because of account.cash.statement + that need it.""" + if 'import_config_id' in vals: + profil_obj = self.pool.get('account.statement.profil') + profile = profil_obj.browse(cr,uid,vals['import_config_id'],context) + vals['journal_id'] = profile.journal_id.id + return super(AccountBankSatement, self).create(cr, uid, vals, context=context) + def _get_period(self, cursor, uid, date, context=None): ''' Find matching period for date, used in thestatement line creation. @@ -68,18 +153,28 @@ class AccountSatement(osv.osv): _constraints = [ (_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']), ] - + def create_move_from_st_line(self, cr, uid, st_line_id, company_currency_id, st_line_number, context=None): """Override a large portion of the code to compute the periode for each line instead of taking the period of the whole statement. - Remove the entry posting on generated account moves.""" + Remove the entry posting on generated account moves. + Point to account.bank.statement.line instead of account.bank.statement.line. + In Treasury Statement, unlike the Bank statement, we will change the move line generated from the + lines depending on the profil (config import): + - If receivable_account_id is set, we'll use it instead of the "partner" one + - If partner_id is set, we'll us it for the commission (when imported throufh the wizard) + - If partner_id is set and force_partner_on_bank is ticked, we'll let the partner of each line + for the debit line, but we'll change it on the credit move line for the choosen partner_id + => This will ease the reconsiliation process with the bank as the partner will match the bank + statement line + """ if context is None: context = {} res_currency_obj = self.pool.get('res.currency') account_move_obj = self.pool.get('account.move') account_move_line_obj = self.pool.get('account.move.line') - account_bank_statement_line_obj = self.pool.get('account.bank.statement.line') - st_line = account_bank_statement_line_obj.browse(cr, uid, st_line_id, context=context) + account_bank_statement_line_obj = self.pool.get('account.bank.statement.line') # Chg + st_line = account_bank_statement_line_obj.browse(cr, uid, st_line_id, context=context) # Chg st = st_line.statement_id context.update({'date': st_line.date}) @@ -95,7 +190,7 @@ class AccountSatement(osv.osv): 'name': st_line_number, 'ref': st_line.ref, }, context=context) - account_bank_statement_line_obj.write(cr, uid, [st_line.id], { + account_bank_statement_line_obj.write(cr, uid, [st_line.id], { # Chg 'move_ids': [(4, move_id, False)] }) @@ -121,7 +216,8 @@ class AccountSatement(osv.osv): 'account_id': (st_line.account_id) and st_line.account_id.id, 'credit': ((amount>0) and amount) or 0.0, 'debit': ((amount<0) and -amount) or 0.0, - 'statement_id': st.id, + # Replace with the treasury one instead of bank #Chg + 'statement_id': st.id, 'journal_id': st.journal_id.id, 'period_id': period_id, #Chg 'currency_id': st.currency.id, @@ -149,16 +245,23 @@ class AccountSatement(osv.osv): if st.currency.id <> company_currency_id: amount_currency = st_line.amount currency_id = st.currency.id + # GET THE RIGHT PARTNER ACCORDING TO THE CHOSEN PROFIL # Chg + if st.import_config_id.force_partner_on_bank: # Chg + bank_parrtner_id = st.import_config_id.partner_id.id # Chg + else: # Chg + bank_parrtner_id = ((st_line.partner_id) and st_line.partner_id.id) or False # Chg + account_move_line_obj.create(cr, uid, { 'name': st_line.name, 'date': st_line.date, 'ref': st_line.ref, 'move_id': move_id, - 'partner_id': ((st_line.partner_id) and st_line.partner_id.id) or False, + 'partner_id': bank_parrtner_id, # Chg 'account_id': account_id, 'credit': ((amount < 0) and -amount) or 0.0, 'debit': ((amount > 0) and amount) or 0.0, - 'statement_id': st.id, + # Replace with the treasury one instead of bank #Chg + 'statement_id': st.id, 'journal_id': st.journal_id.id, 'period_id': period_id, #Chg 'amount_currency': amount_currency, @@ -199,12 +302,12 @@ class AccountSatement(osv.osv): if context is None: context = {} for st in self.browse(cr, uid, ids, context=context): - + j_type = st.journal_id.type company_currency_id = st.journal_id.company_id.currency_id.id if not self.check_status_condition(cr, uid, st.state, journal_type=j_type): continue - + self.balance_check(cr, uid, st.id, journal_type=j_type, context=context) if (not st.journal_id.default_credit_account_id) \ or (not st.journal_id.default_debit_account_id): @@ -256,11 +359,236 @@ class AccountSatement(osv.osv): self.log(cr, uid, st.id, _('Statement %s is confirmed, journal items are created.') % (st_number,)) return self.write(cr, uid, ids, {'state':'confirm'}, context=context) -class AccountSatementLine(osv.osv): - ''' - Adds the period on line, matched on the date. - ''' - _inherit = 'account.bank.statement.line' + def get_partner_from_so(self, cursor, uid,transaction_id): + """Look for the SO that has the given transaction_id, if not + found, try to match the SO name instead. If still nothing, + return False""" + so_obj = self.pool.get('sale.order') + so_id = so_obj.search(cursor, uid, [('transaction_id', '=', transaction_id)]) + if so_id and len(so_id) == 1: + return so_obj.browse(cursor, uid, so_id[0]).partner_id.id + else: + so_id2 = so_obj.search(cursor, uid, [('name', '=', transaction_id)]) + if so_id2 and len(so_id2) == 1: + return so_obj.browse(cursor, uid, so_id2[0]).partner_id.id + return False + + + def get_default_accounts(self, cursor, uid, receivable_account_id, context=None): + """We try to determine default accounts if not receivable_account_id set, otherwise + take it for both receivable and payable account""" + 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 + return account_receivable, account_payable + + def _get_account_id(self, cursor, uid, + amount, account_receivable, account_payable): + "return the default account to be used by statement line" + account_id = False + if amount >= 0: + account_id = account_receivable + else: + account_id = account_payable + if not account_id: + raise osv.except_osv( + _('Can not determine account'), + _('Please ensure that minimal properties are set') + ) + return account_id + + def balance_check(self, cr, uid, st_id, journal_type='bank', context=None): + """Balance check depends on the profil. If no check for this profil is required, + return True""" + st = self.browse(cr, uid, st_id, context=context) + if st.balance_check: + return super(AccountBankSatement,self).balance_check(cr, uid, st_id, journal_type, context) + else: + return True + + def _get_value_from_import_config(self, cr, uid, import_config_id): + """Return a dict with with values taken from the given config. + e.g. (journal_id, partner_id, commission_account_id, mode, forced_account_id) + """ + # Get variable from config + import_config = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id) + forced_account_id = import_config.receivable_account_id and import_config.receivable_account_id.id or False + journal_id = import_config.journal_id and import_config.journal_id.id or False + partner_id = import_config.partner_id and import_config.partner_id.id or False + commission_account_id = import_config.commission_account_id.id + commission_analytic_id = import_config.commission_analytic_id and import_config.commission_analytic_id.id or False + force_partner_on_bank = import_config.force_partner_on_bank + return journal_id, partner_id, commission_account_id, commission_analytic_id, forced_account_id, force_partner_on_bank + + def onchange_imp_config_id(self, cr, uid, ids, import_config_id, context=None): + if not import_config_id: + return {} + import_config = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id) + journal_id = import_config.journal_id.id + account_id = import_config.journal_id.default_debit_account_id.id + credit_partner_id = import_config.partner_id and import_config.partner_id.id or False + return {'value': {'journal_id':journal_id, 'account_id': account_id, + 'balance_check':import_config.balance_check, + 'credit_partner_id':credit_partner_id, + }} + + def credit_statement_import(self, cursor, uid, ids, + import_config_id, + file_stream, + ftype="csv", + context=None): + "Create statement from file stream encoded in base 64" + 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') + + # Get variable from config + journal_id, partner_id, commission_account_id, commission_analytic_id, \ + forced_account_id, force_partner_on_bank = self._get_value_from_import_config(cursor,uid,import_config_id) + + account_receivable, account_payable = self.get_default_accounts(cursor, uid, forced_account_id) + + ##Order of cols does not matter but first row has to be header + keys = ['transaction_id', 'label', 'date', 'amount', 'commission_amount'] + #required_values = ['transaction_id', 'amount', 'commission_amount'] + convertion_dict = { + 'transaction_id': unicode, + 'label': unicode, + 'date': datetime.datetime, + 'amount': float, + 'commission_amount': float + } + + f_parser = FileParser(file_stream, + keys_to_validate=keys, + decode_base_64=True, + ftype=ftype) + statement_lines = f_parser.parse() + statement_lines = f_parser.cast_rows(statement_lines, convertion_dict) + journal = self.pool.get('account.journal').browse(cursor, uid, journal_id) + statement_id = statement_obj.create(cursor, + uid, + { 'import_config_id':import_config_id, + 'journal_id': journal_id, + 'journal_id': journal_id, + 'credit_partner_id': partner_id, + 'statement_type': 'credit_partner', + }, + context) + commission_global_amount = 0.0 + if not journal.default_debit_account_id \ + or not journal.default_credit_account_id: + raise osv.except_osv( + _("Missing default account on journal %s")%(journal.name), + _("Please correct the journal")) + try: + for line in statement_lines: + line_partner_id = False + line_to_reconcile = False + # We ensure that required values of the line are set +# for val in required_values: +# if not line.get(val, False) and line.get(val, False) != 0.0: +# raise osv.except_osv( +# _("Field %s not set for line %s")%(str(line),), +# _("Please correct the file")) + + commission_global_amount += line.get('commission_amount', 0.0) + values = { + 'name': "IN %s %s"%(line['transaction_id'], + line.get('label', '')), + 'date': line.get('date', datetime.datetime.now().date()), + 'amount': line['amount'], + 'ref': "TID_%s"%(line['transaction_id'],), + 'type': 'customer', + 'statement_id': statement_id, + #'account_id': journal.default_debit_account_id + } + values['account_id'] = self._get_account_id( + cursor, + uid, + line['amount'], + account_receivable, + account_payable + ) + if not line_partner_id: + line_partner_id = self.get_partner_from_so(cursor, + uid, line['transaction_id']) + values['partner_id'] = line_partner_id + # we finally create the line in system + statement_line_obj.create(cursor, uid, values, context=context) + + # 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 + ) + 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 AccountBankSatementLine(Model): + _inherit = "account.bank.statement.line" def _get_period(self, cursor, user, context=None): date = context.get('date', None) @@ -268,9 +596,73 @@ class AccountSatementLine(osv.osv): return periods and periods[0] or False _columns = { + # 'statement_id': fields.many2one('account.bank.statement', 'Statement', + # select=True, required=True, ondelete='cascade'), + # 'move_ids': fields.many2many('account.move', + # 'account_treasury_statement_line_move_rel', 'statement_line_id','move_id', + # 'Moves'), + 'ref': fields.char('Reference', size=32, required=True), 'period_id': fields.many2one('account.period', 'Period', required=True), } - _defaults = { 'period_id': _get_period, } + + # WARNING => Crash cause the super method here calls onchange_type => and then + # we don't call it from the good model.... => We'll need to override the complete method here + def onchange_partner_id(self, cr, uid, ids, partner_id, import_config_id, context=None): + # import pdb;pdb.set_trace() + # if context is None: + # context = {} + # res = super(AccountTreasurySatementLine,self).onchange_partner_id(cr, uid, ids, partner_id, context) + # c = self.pool.get("account.statement.profil").browse(cr,uid,import_config_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 + obj_partner = self.pool.get('res.partner') + if context is None: + context = {} + if not partner_id: + return {} + part = obj_partner.browse(cr, uid, partner_id, context=context) + if not part.supplier and not part.customer: + type = 'general' + elif part.supplier and part.customer: + type = 'general' + else: + if part.supplier == True: + type = 'supplier' + if part.customer == True: + type = 'customer' + res_type = self.onchange_type(cr, uid, ids, partner_id, type, import_config_id, context=context) + 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,import_config_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 + + # TOFIX + def onchange_type(self, cr, uid, line_id, partner_id, type, import_config_id, context=None): + 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,import_config_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 + +# class AccountMoveLine(Model): +# _inherit = "account.move.line" +# +# _columns = { +# 'statement_treasury_id': fields.many2one('account.bank.statement', 'Statement', help="The intermediate statement used for reconciliation", select=1), +# } + + diff --git a/account_statement_ext/statement_view.xml b/account_statement_ext/statement_view.xml index a66915c2..0ea59c0a 100644 --- a/account_statement_ext/statement_view.xml +++ b/account_statement_ext/statement_view.xml @@ -1,51 +1,75 @@ - - - account.bank.statement.form - account.bank.statement - + + + + account.move.line.tree + account.move.line + tree + + + + + + + + + account.move.line + search + + + + + + + + + + account.statement.profil.view + account.statement.profil form - - - - - account.bank.statement.tree.banking - - account.bank.statement - tree - - +
+ + + + + + + + + +
- - account.bank.statement.form.add_period - - account.bank.statement - form + + account.statement.profil.view + account.statement.profil + tree - - - + + + + + + + + + + - - account.bank.statement.form.add_period2 - - account.bank.statement - form - - - - - - - + + Bank Statements Profile + account.statement.profil + form + tree,form - + + + account.bank.statement.line.inherit account.bank.statement @@ -58,6 +82,137 @@ + + + account.bank.statement.search + account.bank.statement + + search + + + + + + + + + + + + + + + + account.bank.statement.tree + account.bank.statement + + tree + + + + + + + + + + + + + account.bank.statement.form + account.bank.statement + + form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/account_statement_ext/wizard/__init__.py b/account_statement_ext/wizard/__init__.py new file mode 100644 index 00000000..3d0490eb --- /dev/null +++ b/account_statement_ext/wizard/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author Nicolas Bessi. 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_ext/wizard/import_statement.py b/account_statement_ext/wizard/import_statement.py new file mode 100644 index 00000000..6c53f804 --- /dev/null +++ b/account_statement_ext/wizard/import_statement.py @@ -0,0 +1,107 @@ +# -*- 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" + _description = __doc__ + _columns = { + + 'import_config_id': fields.many2one('account.statement.profil', + 'Import configuration parameter', + 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." + ), + + } + + def onchange_import_config_id(self, cr, uid, ids, import_config_id, context=None): + res={} + if import_config_id: + c = self.pool.get("account.statement.profil").browse(cr,uid,import_config_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 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) + (shortname, ftype) = os.path.splitext(importer.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')) + sid = self.pool.get( + 'account.bank.statement').credit_statement_import( + cursor, + uid, + False, + importer.import_config_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])]) + return result diff --git a/account_statement_ext/wizard/import_statement_view.xml b/account_statement_ext/wizard/import_statement_view.xml new file mode 100644 index 00000000..77d2dd25 --- /dev/null +++ b/account_statement_ext/wizard/import_statement_view.xml @@ -0,0 +1,44 @@ + + + + + credit.statement.import.config.view + credit.statement.import + form + +
+ + + + + + + + + + + + + + + +