[FIX] count of reconciliations was incorrect for partial reconciliations

(lp:c2c-financial-addons/6.1 rev 24.3.1)
This commit is contained in:
Guewen Baconnier @ Camptocamp
2012-06-26 15:25:27 +02:00
parent 1860b51470
commit b95f9e913d
43 changed files with 2289 additions and 482 deletions

View File

@@ -29,6 +29,21 @@
'description': """
Advanced reconciliation methods for the module account_easy_reconcile.
account_easy_reconcile, which is a dependency, is available in the branch:
lp:~openerp-community-committers/+junk/account-extra-addons
This branch is temporary and will soon be merged with the Akretion master
branch, but the master branch does not already exist. Sorry for the
inconvenience.
In addition to the features implemented in account_easy_reconcile, which are:
- reconciliation facilities for big volume of transactions
- setup different profiles of reconciliation by account
- each profile can use many methods of reconciliation
- this module is also a base to create others reconciliation methods
which can plug in the profiles
- a profile a reconciliation can be run manually or by a cron
- monitoring of reconcilation runs with a few logs
It implements a basis to created advanced reconciliation methods in a few lines
of code.
@@ -38,6 +53,11 @@ Typically, such a method can be:
or name
- Reconcile entries if the partner is equal and the ref match with a pattern
And they allows:
- Reconciliations with multiple credit / multiple debit lines
- Partial reconciliations
- Write-off amount as well
A method is already implemented in this module, it matches on entries:
* Partner
* Ref on credit move lines should be case insensitive equals to the ref or
@@ -46,9 +66,6 @@ A method is already implemented in this module, it matches on entries:
The base class to find the reconciliations is built to be as efficient as
possible.
Reconciliations with multiple credit / debit lines is possible.
Partial reconciliation are generated.
You can choose a write-off amount as well.
So basically, if you have an invoice with 3 payments (one per month), the first
month, it will partial reconcile the debit move line with the first payment, the second
@@ -56,12 +73,13 @@ month, it will partial reconcile the debit move line with 2 first payments,
the third month, it will make the full reconciliation.
This module is perfectly adapted for E-Commerce business where a big volume of
move lines and so, reconciliations, is involved and payments often come from
move lines and so, reconciliations, are involved and payments often come from
many offices.
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': [],
'update_xml': ['easy_reconcile_view.xml'],
'demo_xml': [],
'test': [],
'images': [],

View File

@@ -263,11 +263,11 @@ class easy_reconcile_advanced(AbstractModel):
lines_by_id = dict([(l['id'], l) for l in credit_lines + debit_lines])
for reconcile_group_ids in reconcile_groups:
group_lines = [lines_by_id[lid] for lid in reconcile_group_ids]
reconciled, partial = self._reconcile_lines(
reconciled, full = self._reconcile_lines(
cr, uid, rec, group_lines, allow_partial=True, context=context)
if reconciled and partial:
if reconciled and full:
reconciled_ids += reconcile_group_ids
elif partial:
elif reconciled:
partial_reconciled_ids += reconcile_group_ids
return reconciled_ids, partial_reconciled_ids

View File

@@ -31,7 +31,7 @@ class account_easy_reconcile_method(Model):
_get_all_rec_method(cr, uid, context=context)
methods += [
('easy.reconcile.advanced.ref',
'Advanced method, payment ref matches with ref or name'),
'Advanced. Partner and Ref.'),
]
return methods

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="view_easy_reconcile_form" model="ir.ui.view">
<field name="name">account.easy.reconcile.form</field>
<field name="model">account.easy.reconcile</field>
<field name="type">form</field>
<field name="inherit_id" ref="account_easy_reconcile.account_easy_reconcile_form"/>
<field name="arch" type="xml">
<page name="information" position="inside">
<separator colspan="4" string="Advanced. Partner and Ref"/>
<label string="Match multiple debit vs multiple credit entries. Allow partial reconcilation.
The lines should have the partner, the credit entry ref. is matched vs the debit entry ref. or name." colspan="4"/>
</page>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,22 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import statement

View File

@@ -0,0 +1,70 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{'name': "Bank statement base completion",
'version': '1.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
'depends': ['account_statement_ext'],
'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.
Each bank statement profile can have his own rules to apply respecting a sequence order.
Some basic rules are provided in this module:
1) Match from statement line label (based on partner field 'Bank Statement Label')
2) Match from statement line label (based on partner name)
3) Match from statement line reference (based on SO number)
3) Match from statement line reference (based on Invoice number)
You can easily override this module and add your own rules in your own one. The basic rules only
fullfill the partner, but you can use them to complete all values of the line (in the future, we'll
add rule to automatically match and reconcile the line).
It add as well a label on the bank statement line (on which the pre-define rules can match) and
a char field on the partner called 'Bank Statement Label'. Using the pre-define rules, you'll be
able to match various label for a partner.
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 accounting entry part to make the match.
You can use it with our account_advanced_reconcile module to automatize the reconciliation process.
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': [
'statement_view.xml',
'partner_view.xml',
'data.xml',
],
'demo_xml': [],
'test': [],
'installable': True,
'images': [],
'auto_install': False,
'license': 'AGPL-3',
'active': False,
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="bank_statement_completion_rule_2" model="account.statement.completion.rule">
<field name="name">Match from line label (based on partner field 'Bank Statement Label')</field>
<field name="sequence">60</field>
<field name="function_to_call">get_from_label_and_partner_field</field>
</record>
<record id="bank_statement_completion_rule_3" model="account.statement.completion.rule">
<field name="name">Match from line label (based on partner name)</field>
<field name="sequence">70</field>
<field name="function_to_call">get_from_label_and_partner_name</field>
</record>
<record id="bank_statement_completion_rule_1" model="account.statement.completion.rule">
<field name="name">Match from line reference (based on SO number)</field>
<field name="sequence">50</field>
<field name="function_to_call">get_from_ref_and_so</field>
</record>
<record id="bank_statement_completion_rule_4" model="account.statement.completion.rule">
<field name="name">Match from line reference (based on Invoice number)</field>
<field name="sequence">40</field>
<field name="function_to_call">get_from_ref_and_invoice</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,38 @@
# -*- encoding: utf-8 -*-
#################################################################################
# #
# Copyright (C) 2011 Akretion & Camptocamp
# Author : Sébastien BEAU, Joel Grand-Guillaume #
# #
# 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 <http://www.gnu.org/licenses/>. #
# #
#################################################################################
from osv import fields, osv
class res_partner(osv.osv):
"""
Add a bank label on the partner so that we can use it to match
this partner when we found this in a statement line.
"""
_inherit = 'res.partner'
_columns = {
'bank_statement_label':fields.char('Bank Statement Label', size=100,
help="Enter the various label found on your bank statement separated by a ; If \
one of this label is include in the bank statement line, the partner will be automatically \
filled (as long as you use this method/rules in your statement profile)."),
}
res_partner()

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="bk_view_partner_form" model="ir.ui.view">
<field name="name">account_bank_statement_import.view.partner.form</field>
<field name="model">res.partner</field>
<field name="type">form</field>
<field name="priority">20</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml">
<field name="property_account_payable" position="after">
<field name="bank_statement_label"/>
</field>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,361 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from tools.translate import _
import netsvc
logger = netsvc.Logger()
from openerp.osv.orm import Model, fields
from openerp.osv import fields, osv
from operator import itemgetter, attrgetter
class ErrorTooManyPartner(Exception):
"""
New Exception definition that is raised when more than one partner is matched by
the completion rule.
"""
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
class AccountStatementProfil(Model):
"""
Extend the class to add rules per profil that will match at least the partner,
but it could also be used to match other values as well.
"""
_inherit = "account.statement.profil"
_columns={
# @Akretion : For now, we don't implement this features, but this would probably be there:
# 'auto_completion': fields.text('Auto Completion'),
# 'transferts_account_id':fields.many2one('account.account', 'Transferts Account'),
# => You can implement it in a module easily, we design it with your needs in mind
# as well !
'rule_ids':fields.many2many('account.statement.completion.rule',
string='Related statement profiles',
rel='as_rul_st_prof_rel',
),
}
def find_values_from_rules(self, cr, uid, id, line_id, context=None):
"""
This method will execute all related rules, in their sequence order,
to retrieve all the values returned by the first rules that will match.
:param int/long line_id: id of the concerned account.bank.statement.line
:return:
A dict of value that can be passed directly to the write method of
the statement line or {}
{'partner_id': value,
'account_id' : value,
...}
"""
if not context:
context={}
res = {}
rule_obj = self.pool.get('account.statement.completion.rule')
profile = self.browse(cr, uid, id, context=context)
# We need to respect the sequence order
sorted_array = sorted(profile.rule_ids, key=attrgetter('sequence'))
for rule in sorted_array:
method_to_call = getattr(rule_obj, rule.function_to_call)
result = method_to_call(cr,uid,line_id,context)
if result:
return result
return res
class AccountStatementCompletionRule(Model):
"""
This will represent all the completion method that we can have to
fullfill the bank statement lines. You'll be able to extend them in you own module
and choose those to apply for every statement profile.
The goal of a rule 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
or any reference that will be matched by the invoice accounting move.
"""
_name = "account.statement.completion.rule"
_order = "sequence asc"
def _get_functions(self, cr, uid, context=None):
"""
List of available methods for rules. Override this to add you own.
"""
return [
('get_from_ref_and_invoice', 'From line reference (based on invoice number)'),
('get_from_ref_and_so', 'From line reference (based on SO number)'),
('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', help="Lower means paresed first."),
'name': fields.char('Name', size=128),
'profile_ids': fields.many2many('account.statement.profil',
rel='as_rul_st_prof_rel',
string='Related statement profiles'),
'function_to_call': fields.selection(_get_functions, 'Method'),
}
def get_from_ref_and_invoice(self, cursor, uid, line_id, context=None):
"""
Match the partner based on the invoice number and the reference of the statement
line. Then, call the generic get_values_for_line method to complete other values.
If more than one partner matched, raise the ErrorTooManyPartner error.
:param int/long line_id: id of the concerned account.bank.statement.line
:return:
A dict of value that can be passed directly to the write method of
the statement line or {}
{'partner_id': value,
'account_id' : value,
...}
"""
st_obj = self.pool.get('account.bank.statement.line')
st_line = st_obj.browse(cursor,uid,line_id)
res = {}
if st_line:
inv_obj = self.pool.get('account.invoice')
inv_id = inv_obj.search(cursor, uid, [('number', '=', st_line.ref)])
if inv_id:
if inv_id and len(inv_id) == 1:
inv = inv_obj.browse(cursor, uid, inv_id[0])
res['partner_id'] = inv.partner_id.id
elif inv_id and len(inv_id) > 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(cursor, 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_ref_and_so(self, cursor, uid, line_id, context=None):
"""
Match the partner based on the SO number and the reference of the statement
line. Then, call the generic get_values_for_line method to complete other values.
If more than one partner matched, raise the ErrorTooManyPartner error.
:param int/long line_id: id of the concerned account.bank.statement.line
:return:
A dict of value that can be passed directly to the write method of
the statement line or {}
{'partner_id': value,
'account_id' : value,
...}
"""
st_obj = self.pool.get('account.bank.statement.line')
st_line = st_obj.browse(cursor,uid,line_id)
res = {}
if st_line:
so_obj = self.pool.get('sale.order')
so_id = so_obj.search(cursor, uid, [('name', '=', st_line.ref)])
if so_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
elif so_id and len(so_id) > 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(cursor, 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_field(self, cursor, 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 ; Then, call the generic
get_values_for_line method to complete other values.
If more than one partner matched, raise the ErrorTooManyPartner error.
:param int/long line_id: id of the concerned account.bank.statement.line
:return:
A dict of value that can be passed directly to the write method of
the statement line or {}
{'partner_id': value,
'account_id' : value,
...}
"""
partner_obj = self.pool.get('res.partner')
st_obj = self.pool.get('account.bank.statement.line')
st_line = st_obj.browse(cursor,uid,line_id)
res = {}
compt = 0
if st_line:
ids = partner_obj.search(cursor, uid, [['bank_statement_label', '!=', False]], context=context)
for partner in self.browse(cursor, 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))
if res:
st_vals = st_obj.get_values_for_line(cursor, 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, cursor, uid, line_id, context=None):
"""
Match the partner based on the label field of the statement line
and the name of the partner.
Then, call the generic get_values_for_line method to complete other values.
If more than one partner matched, raise the ErrorTooManyPartner error.
:param int/long line_id: id of the concerned account.bank.statement.line
:return:
A dict of value that can be passed directly to the write method of
the statement line or {}
{'partner_id': value,
'account_id' : value,
...}
"""
# This Method has not been tested yet !
res = {}
st_obj = self.pool.get('account.bank.statement.line')
st_line = st_obj.browse(cursor,uid,line_id)
if st_line:
sql = "SELECT id FROM res_partner WHERE name ~* '.*%s.*'"
cursor.execute(sql, (st_line.label,))
result = cursor.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
if res:
st_vals = st_obj.get_values_for_line(cursor, 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
class AccountStatementLine(Model):
"""
Add sparse field on the statement line to allow to store all the
bank infos that are given by a bank/office. You can then add you own in your
module. The idea here is to store all bank/office infos in the additionnal_bank_fields
serialized field when importing the file. If many values, add a tab in the bank
statement line to store your specific one. Have a look in account_statement_base_import
module to see how we've done it.
"""
_inherit = "account.bank.statement.line"
_columns={
'additionnal_bank_fields' : fields.serialized('Additionnal infos from bank',
help="Used by completion and import system. Adds every field that is present in your bank/office \
statement file"),
'label': fields.sparse(type='char', string='Label',
serialization_field='additionnal_bank_fields',
help="Generiy field to store a label given from the bank/office on which we can \
base the default/standard providen rule."),
'already_completed': fields.boolean("Auto-Completed",
help="When this checkbox is ticked, the auto-completion process/button will ignore this line."),
}
_defaults = {
'already_completed': False,
}
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 rules setted on
the profile.. We will ignore line for which already_completed is ticked.
:return:
A dict of value that can be passed directly to the write method of
the statement line or {}
{'partner_id': value,
'account_id' : value,
...}
"""
profile_obj = self.pool.get('account.statement.profil')
st_obj = self.pool.get('account.bank.statement.line')
res={}
errors_stack = []
for line in self.browse(cr,uid, ids, context):
if not line.already_completed:
try:
# Take the default values
res[line.id] = st_obj.get_values_for_line(cr, uid, profile_id = line.statement_id.profile_id.id,
line_type = line.type, amount = line.amount, context = context)
# Ask the rule
vals = profile_obj.find_values_from_rules(cr, uid, line.statement_id.profile_id.id, line.id, context)
# Merge the result
res[line.id].update(vals)
except ErrorTooManyPartner, exc:
msg = "Line ID %s had following error: %s" % (line.id, str(exc))
errors_stack.append(msg)
if errors_stack:
msg = u"\n".join(errors_stack)
raise ErrorTooManyPartner(msg)
return res
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 fullfill.
"""
_inherit = "account.bank.statement"
def button_auto_completion(self, cr, uid, ids, context=None):
"""
Complete line with values given by rules and tic the already_completed
checkbox so we won't compute them again unless the user untick them !
"""
# TODO: Test the errors system, we should be able to complete all line that
# passed, and raise an error for all other at once..
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()
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.get(id, False)
if vals:
vals['already_completed'] = True
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

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="bank_statement_view_form" model="ir.ui.view">
<field name="name">account_bank_statement_import_base.bank_statement.view_form</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field eval="16" name="priority"/>
<field name="type">form</field>
<field name="arch" type="xml">
<data>
<xpath expr="/form/notebook/page/field[@name='line_ids']/tree/field[@name='sequence']" position="before">
<field name="already_completed" />
</xpath>
<xpath expr="/form/notebook/page/field[@name='line_ids']/form/field[@name='sequence']" position="after">
<separator colspan="4" string="Importation related infos"/>
<field name="label" />
<field name="already_completed" />
</xpath>
<xpath expr="/form/group[2]" position="attributes">
<attribute name="col">10</attribute>
</xpath>
<xpath expr="/form/group/field[@name='balance_end']" position="after">
<button name="button_auto_completion" string="Auto Completion" states='draft,open' type="object" colspan="1"/>
</xpath>
</data>
</field>
</record>
<record id="statement_rules_view_form" model="ir.ui.view">
<field name="name">account.statement.profil.view</field>
<field name="model">account.statement.profil</field>
<field name="inherit_id" ref="account_statement_ext.statement_importer_view_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="bank_statement_prefix" position="after">
<separator colspan="4" string="Auto-Completion Rules"/>
<field name="rule_ids" colspan="4" nolabel="1"/>
</field>
</field>
</record>
<record id="statement_st_completion_rule_view_form" model="ir.ui.view">
<field name="name">account.statement.completion.rule.view</field>
<field name="model">account.statement.completion.rule</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Statement Completion Rule">
<field name="sequence"/>
<field name="name" select="1" />
<field name="function_to_call"/>
<separator colspan="4" string="Related Profiles"/>
<field name="profile_ids" nolabel="1" colspan="4"/>
</form>
</field>
</record>
<record id="statement_st_completion_rule_view_tree" model="ir.ui.view">
<field name="name">account.statement.completion.rule.view</field>
<field name="model">account.statement.completion.rule</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Statement Completion Rule">
<field name="sequence"/>
<field name="name" select="1" />
<field name="profile_ids" />
<field name="function_to_call"/>
</tree>
</field>
</record>
<record id="action_st_completion_rule_tree" model="ir.actions.act_window">
<field name="name">Statement Completion Rule</field>
<field name="res_model">account.statement.completion.rule</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem string="Statement Completion Rule" action="action_st_completion_rule_tree"
id="menu_action_st_completion_rule_tree_menu" parent="account.menu_configuration_misc"
sequence="30"/>
</data>
</openerp>

View File

@@ -0,0 +1,23 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import parser
import wizard
import statement

View File

@@ -0,0 +1,68 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{'name': "Bank statement base import",
'version': '1.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
'depends': ['account_statement_ext','account_statement_base_completion'],
'description': """
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 gives you a basic way of importing bank statement through a standard file is providen.
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. That means,
you'll have to choose a file format for each profile.
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.
The goal is here to populate the statement lines of a bank statement with the infos that the
bank or office give you. Fell free to inherit from this module to add your own format.Then,
if you need to complete data from there, add your own account_statement_*_completion module and implement
the needed rules.
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': [
"wizard/import_statement_view.xml",
"statement_view.xml",
],
'demo_xml': [],
'test': [],
'installable': True,
'images': [],
'auto_install': False,
'license': 'AGPL-3',
'active': False,
}

View File

@@ -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"
1 ref date amount commission_amount label
2 50969286 2011-03-07 13:45:14 118.4 -11.84 label a
3 51065326 2011-03-05 13:45:14 189 -15.12 label b
4 51179306 2011-03-02 17:45:14 189 -15.12 label c

Binary file not shown.

View File

@@ -0,0 +1,25 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from parser import new_bank_statement_parser
from parser import BankStatementImportParser
import file_parser
import generic_file_parser

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright Camptocamp SA
# Author Nicolas Bessi
# 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
@@ -18,65 +18,95 @@
#
##############################################################################
"""
Parser for csv or xml for file containing credit transfert data from
financial institure as VISA
"""
from tools.translate import _
from openerp.tools.translate import _
import base64
import csv
import tempfile
import datetime
from parser import BankStatementImportParser
from parser import UnicodeDictReader
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'):
class FileParser(BankStatementImportParser):
"""
Generic abstract class for defining parser for .csv or .xls file format.
"""
def __init__(self, parse_name, keys_to_validate=[], ftype='csv', convertion_dict=None, *args, **kwargs):
"""
:param char: parse_name : The name of the parser
:param list: keys_to_validate : contain the key that need to be present in the file
:param char ftype: extension of the file (could be csv or xls)
: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(FileParser, self).__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.decode_base_64 = decode_base_64
if filebuffer:
self.filebuffer = filebuffer
else:
raise Exception(_('No buffer file'))
self.convertion_dict = convertion_dict
def parse(self):
"launch parsing of csv or xls"
if self.decode_base_64:
self._decode_64b_stream()
def _custom_format(self, *args, **kwargs):
"""
No other work on data are needed in this parser.
"""
return True
def _pre(self, *args, **kwargs):
"""
No pre-treatment needed for this parser.
"""
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()
res = self._parse_csv()
else:
res = self.parse_xls()
if self.keys_to_validate:
self._validate_column(res, self.keys_to_validate)
return res
res = self._parse_xls()
self.result_row_list = res
return True
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:
def _validate(self, *args, **kwargs):
"""
We check that all the key of the given file (means header) are present
in the validation key providen. Otherwise, we raise an Exception.
"""
parsed_cols = self.result_row_list[0].keys()
for col in self.keys_to_validate:
if col not in parsed_cols:
raise Exception(_('col %s not present in file') % (col))
raise Exception(_('Column %s not present in file') % (col))
return True
def parse_csv(self, delimiter=';'):
"return an array of dict from csv file"
def _post(self, *args, **kwargs):
"""
Cast row type depending on the file format .csv or .xls after parsing the file.
"""
self.result_row_list = self._cast_rows(*args, **kwargs)
return True
def _parse_csv(self, delimiter=';'):
"""
:return: dict of dict from csv file (line/rows)
"""
csv_file = tempfile.NamedTemporaryFile()
csv_file.write(self.filebuffer)
# We ensure that cursor is at beginig of file
@@ -87,8 +117,10 @@ class FileParser(object):
)
return [x for x in reader]
def parse_xls(self):
"return an array of dict from csv file"
def _parse_xls(self):
"""
:return: dict of dict from xls file (line/rows)
"""
wb_file = tempfile.NamedTemporaryFile()
wb_file.write(self.filebuffer)
# We ensure that cursor is at beginig of file
@@ -106,6 +138,10 @@ class FileParser(object):
return res
def _from_csv(self, result_set, conversion_rules):
"""
Handle the converstion from the dict and handle date format from
an .csv file.
"""
for line in result_set:
for rule in conversion_rules:
if conversion_rules[rule] == datetime.datetime:
@@ -117,6 +153,10 @@ class FileParser(object):
return result_set
def _from_xls(self, result_set, conversion_rules):
"""
Handle the converstion from the dict and handle date format from
an .xls file.
"""
for line in result_set:
for rule in conversion_rules:
if conversion_rules[rule] == datetime.datetime:
@@ -126,6 +166,11 @@ class FileParser(object):
line[rule] = conversion_rules[rule](line[rule])
return result_set
def cast_rows(self, result_set, conversion_rules):
def _cast_rows(self, *args, **kwargs):
"""
Convert the self.result_row_list using the self.convertion_dict providen.
We call here _from_xls or _from_csv depending on the self.ftype variable.
"""
func = getattr(self, '_from_%s'%(self.ftype))
return func(result_set, conversion_rules)
res = func(self.result_row_list, self.convertion_dict)
return res

View File

@@ -0,0 +1,102 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.tools.translate import _
import base64
import csv
import tempfile
import datetime
# from . import file_parser
from file_parser import FileParser
try:
import xlrd
except:
raise Exception(_('Please install python lib xlrd'))
class GenericFileParser(FileParser):
"""
Standard parser that use a define format in csv or xls to import into a
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, 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(GenericFileParser,self).__init__(parse_name, keys_to_validate=keys_to_validate, ftype=ftype, convertion_dict=convertion_dict)
@classmethod
def parser_for(cls, parser_name):
"""
Used by the new_bank_statement_parser class factory. Return true if
the providen name is generic_csvxls_so
"""
return parser_name == 'generic_csvxls_so'
def get_st_line_vals(self, line, *args, **kwargs):
"""
This method must return a dict of vals that can be passed to create
method of statement line in order to record it. It is the responsibility
of every parser to give this dict of vals, so each one can implement his
own way of recording the lines.
:param: line: a dict of vals that represent a line of result_row_list
:return: dict of values to give to the create method of statement line,
it MUST contain at least:
{
'name':value,
'date':value,
'amount':value,
'ref':value,
'label':value,
'commission_amount':value,
}
In this generic parser, the commission is given for every line, so we store it
for each one.
"""
return {
'name': line.get('label', line.get('ref','/')),
'date': line.get('date', datetime.datetime.now().date()),
'amount': line.get('amount', 0.0),
'ref': line.get('ref','/'),
'label': line.get('label',''),
'commission_amount': line.get('commission_amount', 0.0),
}
def _post(self, *args, **kwargs):
"""
Compute the commission from value of each line
"""
res = super(GenericFileParser, self)._post(*args, **kwargs)
val = 0.0
for row in self.result_row_list:
val += row.get('commission_amount',0.0)
self.commission_global_amount = val
return res

View File

@@ -0,0 +1,213 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import base64
import csv
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):
"""
Generic abstract class for defining parser for different files and
format to import in a bank statement. Inherit from it to create your
own. If your file is a .csv or .xls format, you should consider inheirt
from the FileParser instead.
"""
def __init__(self, parser_name, *args, **kwargs):
# The name of the parser as it will be called
self.parser_name = parser_name
# The result as a list of row. One row per line of data in the file, but
# not the commission one !
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):
"""
Override this method for every new parser, so that new_bank_statement_parser can
return the good class from his name.
"""
return False
def _decode_64b_stream(self):
"""
Decode self.filebuffer in base 64 and override it
"""
self.filebuffer = base64.b64decode(self.filebuffer)
return True
def _format(self, decode_base_64=True, **kwargs):
"""
Decode into base 64 if asked and Format the given filebuffer by calling
_custom_format method.
"""
if decode_base_64:
self._decode_64b_stream()
self._custom_format(kwargs)
return True
def _custom_format(self, *args, **kwargs):
"""
Implement a method in your parser to convert format, encoding and so on before
starting to work on datas. Work on self.filebuffer
"""
return NotImplementedError
def _pre(self, *args, **kwargs):
"""
Implement a method in your parser to make a pre-treatment on datas before parsing
them, like concatenate stuff, and so... Work on self.filebuffer
"""
return NotImplementedError
def _parse(self, *args, **kwargs):
"""
Implement a method in your parser to save the result of parsing self.filebuffer
in self.result_row_list instance property.
"""
return NotImplementedError
def _validate(self, *args, **kwargs):
"""
Implement a method in your parser 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 in your parser to make some last changes on the result of parsing
the datas, like converting dates, computing commission, ...
Work on self.result_row_list and put the commission global amount if any
in the self.commission_global_amount one.
"""
return NotImplementedError
def get_st_line_vals(self, line, *args, **kwargs):
"""
Implement a method in your parser that must return a dict of vals that can be
passed to create method of statement line in order to record it. It is the responsibility
of every parser to give this dict of vals, so each one can implement his
own way of recording the lines.
:param: line: a dict of vals that represent a line of result_row_list
:return: dict of values to give to the create method of statement line,
it MUST contain at least:
{
'name':value,
'date':value,
'amount':value,
'ref':value,
}
"""
return NotImplementedError
def get_st_line_commision(self, *args, **kwargs):
"""
This is called by the importation method to create the commission line in
the bank statement. We will always create one line for the commission in the
bank statement, but it could be computated from a value of each line, or given
in a single line for the whole file.
return: float of the whole commission (self.commission_global_amount)
"""
return self.commission_global_amount
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):
"""
Return an instance of the good parser class base on the providen name
:param char: parser_name
:return: class instance of parser_name providen.
"""
for cls in itersubclasses(BankStatementImportParser):
if cls.parser_for(parser_name):
return cls(parser_name, *args, **kwargs)
raise ValueError

View File

@@ -0,0 +1,233 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from tools.translate import _
import datetime
import netsvc
logger = netsvc.Logger()
from openerp.osv.orm import Model, fields
from openerp.osv import fields, osv
# from account_statement_base_import.parser.file_parser import FileParser
from parser import new_bank_statement_parser
import sys
import traceback
class AccountStatementProfil(Model):
_inherit = "account.statement.profil"
def get_import_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.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 write_logs_after_import(self, cr, uid, ids, statement_id, num_lines, context):
"""
Write the log in the logger + in the log field of the profile to report the user about
what has been done.
:param int/long statement_id: ID of the concerned account.bank.statement
:param int/long num_lines: Number of line that have been parsed
:return: True
"""
if type(ids) is int:
ids = [ids]
for id in ids:
log = self.read(cr, uid, id, ['rec_log'], context=context)['rec_log']
log_line = log and log.split("\n") or []
import_date = datetime.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, '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 prepare_global_commission_line_vals(self, cr, uid, parser,
result_row_list, profile, statement_id, context):
"""
Prepare the global commission line if there is one. The global commission is computed by
by calling the get_st_line_commision of the parser. Feel free to override the methode to compute
your own commission line from the result_row_list.
:param: browse_record of the current parser
:param: result_row_list: [{'key':value}]
:param: profile: browserecord of account.statement.profile
:param: statement_id : int/long of the current importing statement ID
:param: context: global context
return: dict of vals that will be passed to create method of statement line.
"""
comm_values = False
if parser.get_st_line_commision():
partner_id = profile.partner_id and profile.partner_id.id or False
commission_account_id = profile.commission_account_id and profile.commission_account_id.id or False
commission_analytic_id = profile.commission_analytic_id and profile.commission_analytic_id.id or False
statement_line_obj = self.pool.get('account.bank.statement.line')
comm_values = {
'name': 'IN '+ _('Commission line'),
'date': datetime.datetime.now().date(),
'amount': parser.get_st_line_commision(),
'partner_id': partner_id,
'type': 'general',
'statement_id': statement_id,
'account_id': commission_account_id,
'ref': 'commission',
'analytic_account_id': commission_analytic_id,
# !! We set the already_completed so auto-completion will not update those values !
'already_completed': True,
}
return comm_values
def prepare_statetement_lines_vals(self, cursor, uid, parser_vals,
account_payable, account_receivable, statement_id, context):
"""
Hook to build the values of a line from the parser returned values. At
least it fullfill the statement_id and account_id. Overide it to add your
own completion if needed.
:param dict of vals from parser for account.bank.statement.line (called by
parser.get_st_line_vals)
:param int/long account_payable: ID of the receivable account to use
:param int/long account_receivable: ID of the payable account to use
:param int/long statement_id: ID of the concerned account.bank.statement
:return : dict of vals that will be passed to create method of statement line.
"""
statement_obj = self.pool.get('account.bank.statement')
values = parser_vals
values['statement_id']= statement_id
values['account_id'] = statement_obj.get_account_for_counterpart(
cursor,
uid,
parser_vals['amount'],
account_receivable,
account_payable
)
return values
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.
It will also create the commission line if it apply and record the providen file as
an attachement of the bank statement.
:param int/long profile_id: ID of the profile used to import the file
:param filebuffer file_stream: binary of the providen file
:param char: ftype represent the file exstension (csv by default)
:return: ID of the created account.bank.statemênt
"""
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 !"))
prof = prof_obj.browse(cursor,uid,profile_id,context)
parser = new_bank_statement_parser(prof.import_type, ftype=ftype)
result_row_list = parser.parse(file_stream)
# Check all key are present in account.bank.statement.line !!
parsed_cols = 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 = statement_obj.get_default_pay_receiv_accounts(cursor, uid, context)
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:
parser_vals = parser.get_st_line_vals(line)
values = self.prepare_statetement_lines_vals(cursor, uid, parser_vals, account_payable,
account_receivable, statement_id, context)
# we finally create the line in system
statement_line_obj.create(cursor, uid, values, context=context)
# Build and create the global commission line for the whole statement
comm_vals = self.prepare_global_commission_line_vals(cursor, uid, parser, result_row_list, prof, statement_id, context)
if comm_vals:
res = statement_line_obj.create(cursor, uid, comm_vals,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(cursor, uid, prof.id, statement_id,
len(result_row_list), context)
except Exception, exc:
statement_obj.unlink(cursor, uid, [statement_id])
error_type, error_value, trbk = sys.exc_info()
st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
st += ''.join(traceback.format_tb(trbk, 30))
raise osv.except_osv(
_("Statement import error"),
_("The statement cannot be created : %s") %(st))
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. In this basic sample case
it concern only commission_amount.
"""
_inherit = "account.bank.statement.line"
_columns={
'commission_amount': fields.sparse(type='float', string='Line Commission Amount',
serialization_field='additionnal_bank_fields'),
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="statement_importer_view_form" model="ir.ui.view">
<field name="name">account.statement.profil.view</field>
<field name="model">account.statement.profil</field>
<field name="inherit_id" ref="account_statement_ext.statement_importer_view_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="bank_statement_prefix" position="after">
<separator colspan="4" string="Import related infos"/>
<field name="launch_import_completion"/>
<field name="last_import_date"/>
<field name="import_type"/>
<button name="%(account_statement_base_import.statement_importer_action)d"
string="Import Bank Statement"
type="action" icon="gtk-ok"
colspan = "2"/>
<separator colspan="4" string="Import Logs"/>
<field name="rec_log" colspan="4" nolabel="1"/>
</field>
</field>
</record>
<record id="bank_statement_view_form" model="ir.ui.view">
<field name="name">account_bank_statement.bank_statement.view_form</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field name="type">form</field>
<field name="arch" type="xml">
<data>
<xpath expr="/form/notebook/page/field[@name='line_ids']/form/field[@name='label']" position="after">
<field name="commission_amount" />
</xpath>
</data>
</field>
</record>
</data>
</openerp>

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author Nicolas Bessi. Copyright Camptocamp SA
# 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

View File

@@ -28,15 +28,25 @@ from tools.translate import _
import os
class CreditPartnerStatementImporter(osv.osv_memory):
"""Import Credit statement"""
_name = "credit.statement.import"
_description = __doc__
def default_get(self, cr, uid, fields, context=None):
if context is None: context = {}
res = {}
if (context.get('active_model', False) == 'account.statement.profil' and
context.get('active_ids', False)):
ids = context['active_ids']
assert len(ids) == 1, 'You cannot use this on more than one profile !'
res['profile_id'] = ids[0]
other_vals = self.onchange_profile_id(cr, uid, [], res['profile_id'], context=context)
res.update(other_vals.get('value',{}))
return res
_columns = {
'import_config_id': fields.many2one('account.statement.profil',
'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',
),
@@ -61,13 +71,12 @@ class CreditPartnerStatementImporter(osv.osv_memory):
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):
def onchange_profile_id(self, cr, uid, ids, profile_id, context=None):
res={}
if import_config_id:
c = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id)
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,
@@ -77,31 +86,30 @@ class CreditPartnerStatementImporter(osv.osv_memory):
'balance_check':c.balance_check,}}
return res
def _check_extension(self, filename):
(shortname, ftype) = os.path.splitext(filename)
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)
(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'))
ftype = self._check_extension(importer.file_name)
sid = self.pool.get(
'account.bank.statement').credit_statement_import(
'account.statement.profil').statement_import(
cursor,
uid,
False,
importer.import_config_id.id,
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])])
return result
)
# We should return here the profile for which we executed the import
return {'type': 'ir.actions.act_window_close'}

View File

@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<form string="Import statement">
<group colspan="4" >
<field name="import_config_id" on_change="onchange_import_config_id(import_config_id)"/>
<field name="profile_id" on_change="onchange_profile_id(profile_id)"/>
<field name="input_statement" filename="file_name" colspan="2"/>
<field name="file_name" colspan="2" invisible="1"/>
<separator string="Import Parameters Summary" colspan="4"/>
@@ -38,7 +38,7 @@
<field name="target">new</field>
</record>
<menuitem id="statement_importer_menu" name="Import Treasury Statement" action="statement_importer_action" parent="account.menu_finance_periodical_processing"/>
<menuitem id="statement_importer_menu" name="Import Bank Statement" action="statement_importer_action" parent="account.menu_finance_bank_and_cash"/>
</data>
</openerp>

View File

@@ -19,7 +19,6 @@
#
##############################################################################
import file_parser
import wizard
import statement
import report
import account

View File

@@ -25,41 +25,51 @@
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
'depends': ['base_transaction_id'],
'depends': ['account'],
'description': """
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.
The goal of this module is to improve the basic bank statement, by adding various new features,
and help dealing with huge volume of reconciliation through payment offices (like Paypal, Lazer,
Visa, Amazon,...).
It will be 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. It will
be the base for all new features developped to improve the reconciliation process (see our other
set of modules, like : account_statement_base_completion, account_statement_base_import,
account_advanced_reconcile).
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:
1) This module improves the bank statement in the way it allows you to define profiles (for each
Office or Bank). The bank statement will then generate the entries based on some criteria chosen
in the selected profile. You can setup on the profile:
- Account commission and partner relation
- Can force an account for the reconciliation
- The journal to use
- Choose to use balance check or not
- Analytic account for commission
- Account commission and Analytic account for commission
- Partner concerned by the profile (used in commission and optionaly on generated credit move)
- Can force a to use a specifi credit account (instead of the receivalble/payable default one)
- 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
2) Adds a report on bank statement that can be used for checks remittance
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.
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
5) Cancelling a bank statement is much more easy and will cancel all related entries, unreconcile them,
and finally delete them.
6) Add the ID in entries view so that you can easily filter on a statement ID to reconcile all related
entries at once (e.g. one statement (ID 100) for paypal on an intermediate account, and then another for
the bank on the bank account. You can then manually reconcile all the line from the first one with
one line of the second by finding them through the statement ID.)
""",
'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',
],

View File

@@ -0,0 +1,40 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import netsvc
logger = netsvc.Logger()
from openerp.osv.orm import Model, fields
class account_move(Model):
_inherit='account.move'
def unlink(self, cr, uid, ids, context=None):
"""
Delete the reconciliation when we delete the moves. This
allow an easier way of cancelling the bank statement.
"""
for move in self.browse(cr, uid, ids, context=context):
for move_line in move.line_id:
if move_line.reconcile_id:
move_line.reconcile_id.unlink(context=context)
return super(account_move, self).unlink(cr, uid, ids, context=context)

View File

@@ -5,10 +5,11 @@
<report auto="False"
id="report_bank_statement_webkit"
model="account.bank.statement"
name="account_statement_import.report_bank_statement_webkit"
file="account_statement_import/report/bank_statement_report.mako"
name="bank_statement_webkit"
file="account_statement_ext/report/bank_statement_report.mako"
string="Bank Statement"
report_type="webkit"
webkit_header="account_statement_ext.bank_statement_landscape_header"
header="1"/>
<record id="action_print_bank_statement_webkit" model="ir.values">

View File

@@ -6,63 +6,63 @@
</style>
</head>
<body>
<%!
def amount(text):
return text.replace('-', '&#8209;') # replace by a non-breaking hyphen (it will not word-wrap between hyphen and numbers)
%>
<%!
def amount(text):
return text.replace('-', '&#8209;') # replace by a non-breaking hyphen (it will not word-wrap between hyphen and numbers)
%>
<%setLang(user.context_lang)%>
%for statement in objects:
<%setLang(user.context_lang)%>
%for statement in objects:
<div class="act_as_table data_table">
<div class="act_as_row labels">
<div class="act_as_cell">${_('Bordereau')}</div>
<div class="act_as_cell">${_('Date')}</div>
<div class="act_as_table data_table">
<div class="act_as_row labels">
<div class="act_as_cell">${_('Bordereau')}</div>
<div class="act_as_cell">${_('Date')}</div>
</div>
<div class="act_as_row">
<div class="act_as_cell">${ statement.name }</div>
<div class="act_as_cell">${ formatLang(statement.date,date=True) }</div>
</div>
</div>
<div class="act_as_row">
<div class="act_as_cell">${ statement.name }</div>
<div class="act_as_cell">${ formatLang(statement.date,date=True) }</div>
</div>
</div>
<!-- we use div with css instead of table for tabular data because div do not cut rows at half at page breaks -->
<div class="act_as_table list_table" style="margin-top: 10px;">
<div class="act_as_thead">
<div class="act_as_row labels">
## date
<div class="act_as_cell first_column" style="width: 100px;">${_('Reference')}</div>
## period
<div class="act_as_cell" style="width: 175px;">${_('Partenaire')}</div>
## move
<div class="act_as_cell" style="width: 60px;">${_('Montant')}</div>
## journal
<!-- we use div with css instead of table for tabular data because div do not cut rows at half at page breaks -->
<div class="act_as_table list_table" style="margin-top: 10px;">
<div class="act_as_thead">
<div class="act_as_row labels">
## date
<div class="act_as_cell first_column" style="width: 100px;">${_('Reference')}</div>
## period
<div class="act_as_cell" style="width: 175px;">${_('Partenaire')}</div>
## move
<div class="act_as_cell" style="width: 60px;">${_('Montant')}</div>
## journal
</div>
</div>
<% sum_statement = 0.0 %>
%for statementline in statement.line_ids:
<div class="act_as_tbody">
## curency code
<div class="act_as_cell">${statementline.name or '' }</div>
## currency balance
<div class="act_as_cell">${statementline.partner_id.name or '' }</div>
## currency balance cumulated
<div class="act_as_cell amount">${formatLang(statementline.amount) | amount }</div>
<% sum_statement += statementline.amount %>
</div>
%endfor
<div class="act_as_tbody">
## curency code
<div class="act_as_cell"></div>
## currency balance
<div class="act_as_cell">Total</div>
## currency balance cumulated
<div class="act_as_cell amount_total">${formatLang(sum_statement) }</div>
</div>
</div>
<% sum_statement = 0.0 %>
%for statementline in statement.line_ids:
<div class="act_as_tbody">
## curency code
<div class="act_as_cell">${statementline.name or '' }</div>
## currency balance
<div class="act_as_cell">${statementline.partner_id.name or '' }</div>
## currency balance cumulated
<div class="act_as_cell amount">${formatLang(statementline.amount) | amount }</div>
<% sum_statement += statementline.amount %>
</div>
%endfor
<div class="act_as_tbody">
## curency code
<div class="act_as_cell"></div>
## currency balance
<div class="act_as_cell">Total</div>
## currency balance cumulated
<div class="act_as_cell amount_total">${formatLang(sum_statement) }</div>
</div>
</div>
%endfor
</div>
%endfor
</body>

View File

@@ -27,8 +27,6 @@ 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):
@@ -66,7 +64,7 @@ class BankStatementWebkit(report_sxw.rml_parse):
statement_lines = statement_obj.browse(self.cr,self.uid,statement_line_ids)
return statement_lines
webkit_report.WebKitParser('report.report_bank_statement_webkit',
webkit_report.WebKitParser('report.bank_statement_webkit',
'account.bank.statement',
'addons/account_statement_import/report/bank_statement_report.mako',
'addons/account_statement_ext/report/bank_statement_report.mako',
parser=BankStatementWebkit)

View File

@@ -20,22 +20,28 @@
##############################################################################
from tools.translate import _
from account_statement_ext.file_parser.parser import FileParser
import datetime
import netsvc
logger = netsvc.Logger()
from openerp.osv.orm import Model, fields
from openerp.osv import fields, osv
class AccountStatementProfil(Model):
"""
A Profile will contain all infos related to the type of
bank statement, and related generated entries. It defines the
journal to use, the partner and commision account and so on.
"""
_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)."),
'Bank/Payment Office 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),
@@ -54,13 +60,16 @@ class AccountStatementProfil(Model):
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."
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."
),
'bank_statement_prefix': fields.char('Bank Statement Prefix', size=32),
'bank_statement_ids': fields.one2many('account.bank.statement', 'profile_id', 'Bank Statement Imported'),
}
_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:
@@ -73,43 +82,42 @@ class AccountStatementProfil(Model):
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."""
"""
We improve the bank statement class mostly for :
- Removing the period and compute it from the date of each line.
- Allow to remove the balance check depending on the chosen profil
- Report errors on confirmation all at once instead of crashing onr by one
- Add a profil notion that can change the generated entries on statement
confirmation.
For this, we had to override quite some long method and we'll need to maintain
them up to date. Changes are point up by '#Chg' comment.
"""
_inherit = "account.bank.statement"
_columns = {
'import_config_id': fields.many2one('account.statement.profil',
'profile_id': fields.many2one('account.statement.profil',
'Profil', required=True, states={'draft': [('readonly', False)]}),
'credit_partner_id': fields.related(
'import_config_id',
'profile_id',
'partner_id',
type='many2one',
relation='res.partner',
string='Financial Partner',
store=True, readonly=True),
'balance_check': fields.related(
'import_config_id',
'profile_id',
'balance_check',
type='boolean',
string='Balance check',
store=True, readonly=True),
'journal_id': fields.related(
'import_config_id',
'profile_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),
}
@@ -119,17 +127,17 @@ class AccountBankSatement(Model):
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:
need it."""
if 'profile_id' in vals:
profil_obj = self.pool.get('account.statement.profil')
profile = profil_obj.browse(cr,uid,vals['import_config_id'],context)
profile = profil_obj.browse(cr,uid,vals['profile_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.
'''
"""
Find matching period for date, used in the statement line creation.
"""
period_obj = self.pool.get('account.period')
periods = period_obj.find(cursor, uid, dt=date, context=context)
return periods and periods[0] or False
@@ -149,24 +157,46 @@ class AccountBankSatement(Model):
return False
return True
# Redefine the constraint, or it still refer to the original method
_constraints = [
(_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']),
]
def button_cancel(self, cr, uid, ids, context={}):
"""
We cancel the related move, delete them and finally put the
statement in draft state. So no need to unreconcile all entries,
then unpost them, then finaly cancel the bank statement.
"""
done = []
for st in self.browse(cr, uid, ids, context=context):
if st.state=='draft':
continue
ids = []
for line in st.line_ids:
for move in line.move_ids:
move.button_cancel(context=context)
move.unlink(context=context)
done.append(st.id)
self.write(cr, uid, done, {'state':'draft'}, context=context)
return True
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
"""
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.
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):
We change the move line generated from the lines depending on the profil:
- 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
=> This will ease the reconciliation process with the bank as the partner will match the bank
statement line
:param int/long: st_line_id: account.bank.statement.line ID
:param int/long: company_currency_id: res.currency ID
:param char: st_line_number: that will be used as the name of the generated account move
:return: int/long: ID of the created account.move
"""
if context is None:
context = {}
@@ -246,8 +276,8 @@ class AccountBankSatement(Model):
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
if st.profile_id.force_partner_on_bank: # Chg
bank_parrtner_id = st.profile_id.partner_id.id # Chg
else: # Chg
bank_parrtner_id = ((st_line.partner_id) and st_line.partner_id.id) or False # Chg
@@ -280,24 +310,40 @@ class AccountBankSatement(Model):
account_move_obj.post(cr, uid, [move_id], context=context)
return move_id
def _get_st_number_period(self, cr, uid, date, journal_sequence_id):
"""Retrieve the name of bank statement from sequence, according to the period
corresponding to the date passed in args"""
def _get_st_number_period_profil(self, cr, uid, date, profile_id):
"""
Retrieve the name of bank statement from sequence, according to the period
corresponding to the date passed in args. Add a prefix if set in the profil.
:param: date: date of the statement used to compute the right period
:param: int/long: profile_id: the account.statement.profil ID from which to take the
bank_statement_prefix for the name
:return: char: name of the bank statement (st_number)
"""
year = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, date)).fiscalyear_id.id
profile = self.pool.get('account.statement.profil').browse(cr,uid, profile_id)
c = {'fiscalyear_id': year}
obj_seq = self.pool.get('ir.sequence')
journal_sequence_id = profile.journal_id.sequence_id and profile.journal_id.sequence_id.id or False
if journal_sequence_id:
st_number = obj_seq.next_by_id(cr, uid, journal_sequence_id, context=c)
else:
st_number = obj_seq.next_by_code(cr, uid, 'account.bank.statement', context=c)
if profile.bank_statement_prefix:
st_number = profile.bank_statement_prefix + st_number
return st_number
def button_confirm_bank(self, cr, uid, ids, context=None):
"""Completely override the method in order to have
an error message which displays all the messages
instead of having them pop one by one.
We have to copy paste a big block of code, changing the error
stack + managing period from date."""
"""
Completely override the method in order to have
an error message which displays all the messages
instead of having them pop one by one.
We have to copy paste a big block of code, changing the error
stack + managing period from date.
TODO: Log the error in a bank statement field instead of using a popup !
"""
# obj_seq = self.pool.get('irerrors_stack.sequence')
if context is None:
context = {}
@@ -318,13 +364,7 @@ class AccountBankSatement(Model):
st_number = st.name
else:
# Begin Changes
seq_id = st.journal_id.sequence_id and st.journal_id.sequence_id.id or False
st_number = self._get_st_number_period(cr, uid, st.date, seq_id)
# c = {'fiscalyear_id': st.period_id.fiscalyear_id.id}
# if st.journal_id.sequence_id:
# st_number = obj_seq.next_by_id(cr, uid, st.journal_id.sequence_id.id, context=c)
# else:
# st_number = obj_seq.next_by_code(cr, uid, 'account.bank.statement', context=c)
st_number = self._get_st_number_period_profil(cr, uid, st.date, st.profile_id.id)
# End Changes
for line in st.move_line_ids:
if line.state <> 'valid':
@@ -359,60 +399,19 @@ class AccountBankSatement(Model):
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)
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,
def get_account_for_counterpart(self, cursor, uid,
amount, account_receivable, account_payable):
"return the default account to be used by statement line"
"""
Give the amount, payable and receivable account (that can be found using
get_default_pay_receiv_accounts method) and receive the one to use. This method
should be use when there is no other way to know which one to take.
:param float: amount of the line
:param int/long: account_receivable the receivable account
:param int/long: account_payable the payable account
:return: int/long :the default account to be used by statement line as the counterpart
of the journal account depending on the amount.
"""
account_id = False
if amount >= 0:
account_id = account_receivable
@@ -425,33 +424,71 @@ class AccountBankSatement(Model):
)
return account_id
def get_default_pay_receiv_accounts(self, cursor, uid, context=None):
"""
We try to determine default payable/receivable accounts to be used as counterpart
from the company default propoerty. This is to be used if there is no otherway to
find the good one, or to find a default value that will be overriden by a completion
method (rules of account_statement_base_completion) afterwards.
:return: tuple of int/long ID that give account_receivable, account_payable based on
company default.
"""
account_receivable = False
account_payable = False
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):
"""Balance check depends on the profil. If no check for this profil is required,
return True"""
"""
Balance check depends on the profil. If no check for this profil is required,
return True and do nothing, otherwise call super.
:param int/long st_id: ID of the concerned account.bank.statement
:param char: journal_type that concern the bank statement
: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:
def onchange_imp_config_id(self, cr, uid, ids, profile_id, context=None):
"""
Compute values on the change of the profile.
:param: int/long: profile_id that changed
:return dict of dict with key = name of the field
"""
if not profile_id:
return {}
import_config = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id)
import_config = self.pool.get("account.statement.profil").browse(cr,uid,profile_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
@@ -460,166 +497,102 @@ class AccountBankSatement(Model):
'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):
"""
Override to compute the period from the date of the line, add a method to retrieve
the values for a line from the profile. Override the on_change method to take care of
the profile when fullfilling the bank statement manually. Set the reference to 64
Char long instead 32.
"""
_inherit = "account.bank.statement.line"
def _get_period(self, cursor, user, context=None):
"""
Return a period from a given date in the context.
"""
date = context.get('date', None)
periods = self.pool.get('account.period').find(cursor, user, dt=date)
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),
# 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,
}
# 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
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.
:param int/long profile_id of the related bank statement
:param int/long partner_id of the line
:param char line_type: a value from: 'general', 'supplier', 'customer'
:param float: amount of the line
: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):
"""
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 = {}
@@ -635,34 +608,23 @@ class AccountBankSatementLine(Model):
type = 'supplier'
if part.customer == True:
type = 'customer'
res_type = self.onchange_type(cr, uid, ids, partner_id, type, import_config_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,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
return {'value': {'type': type, 'account_id': res_type['value']['account_id']}}
return {'value': {'type': type}}
# TOFIX
def onchange_type(self, cr, uid, line_id, partner_id, type, import_config_id, context=None):
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,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})
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
# 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),
# }

View File

@@ -40,6 +40,7 @@
<field name="receivable_account_id" />
<field name="force_partner_on_bank"/>
<field name="balance_check"/>
<field name="bank_statement_prefix"/>
</form>
</field>
</record>
@@ -90,7 +91,7 @@
<field name="type">search</field>
<field name="arch" type="xml">
<xpath expr="/search/group/field[@name='name']" position="before">
<field name="import_config_id"/>
<field name="profile_id"/>
<field name="credit_partner_id"/>
<separator orientation="vertical"/>
</xpath>
@@ -109,7 +110,7 @@
<field name="type">tree</field>
<field name="arch" type="xml">
<xpath expr="/tree/field[@name='name']" position="after">
<field name="import_config_id"/>
<field name="profile_id"/>
</xpath>
<xpath expr="/tree/field[@name='period_id']" position="replace">
<field name="credit_partner_id"/>
@@ -133,7 +134,7 @@
<xpath expr="/form/group[@col='7']" position="before">
<group col="6" colspan="4">
<field name="name" select="1"/>
<field name="import_config_id" select="1" required="1" on_change="onchange_imp_config_id(import_config_id)" widget="selection"/>
<field name="profile_id" select="1" required="1" on_change="onchange_imp_config_id(profile_id)" widget="selection"/>
<field name="date" select="1" on_change="onchange_date(date, company_id)"/>
</group>
<separator string="Details" colspan="4"/>
@@ -166,53 +167,36 @@
<field name="id"/>
</xpath>
<xpath expr="/form/notebook/page/field/tree/field[@name='partner_id']" position="replace">
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.import_config_id)"/>
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.profile_id)"/>
</xpath>
<xpath expr="/form/notebook/page/field/form/field[@name='date']" position="before">
<field name="id"/>
</xpath>
<!-- Adapt onchange signature -->
<xpath expr="/form/notebook/page/field/tree/field[@name='partner_id']" position="replace">
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.import_config_id)"/>
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.profile_id)"/>
</xpath>
<xpath expr="/form/notebook/page/field/form/field[@name='partner_id']" position="replace">
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.import_config_id)"/>
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.profile_id)"/>
</xpath>
<xpath expr="/form/notebook/page/field/form/field[@name='type']" position="replace">
<field name="type" on_change="onchange_type(partner_id, type, parent.import_config_id)"/>
<field name="type" on_change="onchange_type(partner_id, type, parent.profile_id)"/>
</xpath>
<xpath expr="/form/notebook/page/field/tree/field[@name='type']" position="replace">
<field name="type" on_change="onchange_type(partner_id, type, parent.import_config_id)"/>
<field name="type" on_change="onchange_type(partner_id, type, parent.profile_id)"/>
</xpath>
</field>
</record>
<!-- <record id="action_treasury_statement_tree" model="ir.actions.act_window">
<field name="name">Intermediate Statements</field>
<field name="res_model">account.bank.statement</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="domain">[('journal_id.type', '=', 'bank')]</field>
<field name="context">{'journal_type':'bank'}</field>
<field name="search_view_id" ref="view_treasury_statement_search"/>
<field name="help">A treasury statement is a summary of all financial transactions occurring a credit card or any other type of intermediate financial account.</field>
</record>
<record model="ir.actions.act_window.view" id="action_treasury_statement_tree_bank">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_treasury_statement_tree"/>
<field name="act_window_id" ref="action_treasury_statement_tree"/>
</record>
<record model="ir.actions.act_window.view" id="action_treasury_statement_form_bank">
<field name="sequence" eval="1"/>
<field name="view_mode">form</field>
<field name="view_id" ref="view_treasury_statement_form"/>
<field name="act_window_id" ref="action_treasury_statement_tree"/>
</record>
<act_window id="act_bank_statement_from_profile"
name="Open Statements"
res_model="account.bank.statement"
src_model="account.statement.profil"
domain="[('profile_id','=',active_id),]"
view_type="form"/>
<menuitem string="Treasury Statements" action="action_treasury_statement_tree" id="menu_treasury_statement_tree" parent="account.menu_finance_bank_and_cash" sequence="9"/> -->
</data>
</openerp>

View File

@@ -0,0 +1,22 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import statement

View File

@@ -0,0 +1,52 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{'name': "Bank statement completion from transaction ID",
'version': '1.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
'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,
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': True,
'license': 'AGPL-3',
'active': False,
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="bank_statement_completion_rule_4" model="account.statement.completion.rule">
<field name="name">Match from line reference (based on transaction ID)</field>
<field name="sequence">40</field>
<field name="function_to_call">get_from_transaction_id_and_so</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,90 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from tools.translate import _
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 AccountStatementCompletionRule(Model):
"""Add a rule based on transaction ID"""
_inherit = "account.statement.completion.rule"
def _get_functions(self, cr, uid, context=None):
res = super (AccountStatementCompletionRule, self)._get_functions(
cr, uid, context=context)
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'),
}
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.
:param int/long line_id: ID of the concerned account.bank.statement.line
:return:
A dict of value that can be passed directly to the write method of
the statement line or {}
{'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 ErrorTooManyPartner(_('Line named "%s" was matched by more than one partner.')%(st_line.name,st_line.id))
if so_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
class AccountStatementLine(Model):
_inherit = "account.bank.statement.line"
_columns={
# 'additionnal_bank_fields' : fields.serialized('Additionnal infos from bank', help="Used by completion and import system."),
'transaction_id': fields.sparse(type='char', string='Transaction ID',
size=128,
serialization_field='additionnal_bank_fields',
help="Transction id from the financial institute"),
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="bank_statement_view_form" model="ir.ui.view">
<field name="name">account_bank_statement_import_base.bank_statement.view_form</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field eval="16" name="priority"/>
<field name="type">form</field>
<field name="arch" type="xml">
<data>
<xpath expr="/form/notebook/page/field[@name='line_ids']/form/field[@name='label']" position="after">
<field name="transaction_id" />
</xpath>
</data>
</field>
</record>
</data>
</openerp>

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi
# Author: Joel Grand-Guillaume
# Copyright 2011-2012 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
@@ -18,5 +18,4 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import parser

View File

@@ -0,0 +1,60 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{'name': "Bank statement transactionID import",
'version': '1.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
'depends': ['account_statement_base_import','account_statement_transactionid_completion'],
'description': """
This module brings generic methods and fields on bank statement to deal with
the importation of different bank and offices that uses transactionID.
This module 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 account_statement_ext module) to generate the entries.
This module can handle a commission taken by the payment office and has the following format:
* transaction_id : the transaction ID given by the bank/office. 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',
'init_xml': [],
'update_xml': [
],
'demo_xml': [],
'test': [],
'installable': True,
'images': [],
'auto_install': False,
'license': 'AGPL-3',
'active': False,
}

View File

@@ -0,0 +1,24 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
# from parser import new_bank_statement_parser
# from parser import BankStatementImportParser
import transactionid_file_parser

View File

@@ -0,0 +1,99 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.tools.translate import _
import base64
import csv
import tempfile
import datetime
from account_statement_base_import.parser.file_parser import FileParser
try:
import xlrd
except:
raise Exception(_('Please install python lib xlrd'))
class TransactionIDFileParser(FileParser):
"""
TransactionID parser that use a define format in csv or xls to import
bank statement.
"""
def __init__(self, parse_name, ftype='csv'):
convertion_dict = {
'transaction_id': 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 = ['transaction_id', 'label', 'date', 'amount', 'commission_amount']
super(TransactionIDFileParser,self).__init__(parse_name, keys_to_validate=keys_to_validate, ftype=ftype, convertion_dict=convertion_dict)
@classmethod
def parser_for(cls, parser_name):
"""
Used by the new_bank_statement_parser class factory. Return true if
the providen name is generic_csvxls_transaction
"""
return parser_name == 'generic_csvxls_transaction'
def get_st_line_vals(self, line, *args, **kwargs):
"""
This method must return a dict of vals that can be passed to create
method of statement line in order to record it. It is the responsibility
of every parser to give this dict of vals, so each one can implement his
own way of recording the lines.
:param: line: a dict of vals that represent a line of result_row_list
:return: dict of values to give to the create method of statement line,
it MUST contain at least:
{
'name':value,
'date':value,
'amount':value,
'ref':value,
'label':value,
'commission_amount':value,
}
In this generic parser, the commission is given for every line, so we store it
for each one.
"""
return {
'name': line.get('label', line.get('ref','/')),
'date': line.get('date', datetime.datetime.now().date()),
'amount': line.get('amount', 0.0),
'ref': line.get('transaction_id','/'),
'label': line.get('label',''),
'transaction_id': line.get('transaction_id','/'),
'commission_amount': line.get('commission_amount', 0.0),
}
def _post(self, *args, **kwargs):
"""
Compute the commission from value of each line
"""
res = super(GenericFileParser, self)._post(*args, **kwargs)
val = 0.0
for row in self.result_row_list:
val += row.get('commission_amount',0.0)
self.commission_global_amount = val
return res