[REF] refactoring of account_advanced_reconcile using account_easy_reconcile, intermediate commit

(lp:c2c-financial-addons/6.1 rev 24.2.1)
This commit is contained in:
Guewen Baconnier @ Camptocamp
2012-06-12 22:41:47 +02:00
parent 62e866abcc
commit 231f0b276c
56 changed files with 916 additions and 2977 deletions

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2012 Camptocamp SA
# 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
@@ -19,6 +19,4 @@
#
##############################################################################
import easy_reconcile
import base_advanced_reconciliation
import advanced_reconciliation
import reconcile_method

View File

@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
##############################################################################
# -*- coding: utf-8 -*- ##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2012 Camptocamp SA
# 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
@@ -25,66 +24,26 @@
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal',
'depends': ['account_easy_reconcile'],
'depends': ['base_transaction_id', 'account_easy_reconcile'],
'description': """
Advanced reconciliation methods for the module account_easy_reconcile.
This module allows you auto reconcile entries with payment.
It is mostly used in E-Commerce, but could also be useful in other cases.
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.
Typically, such a method can be:
- Reconcile entries if the partner and the ref are equal
- Reconcile entries if the partner is equal and the ref is the same than ref
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
the name of the debit move line
The base class to find the reconciliations is built to be as efficient as
possible.
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
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, are involved and payments often come from
many offices.
The automatic reconciliation matches a transaction ID, if available, propagated from the Sale Order.
It can also search for the sale order name in the origin or description of the move line.
Basically, this module will match account move line with a matching reference on a same account.
It will make a partial reconciliation if more than one move has the same reference (like 3x payments)
Once all payment will be there, it will make a full reconciliation.
You can choose a write-off amount as well.
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': ['easy_reconcile_view.xml'],
'update_xml': [],
'demo_xml': [],
'test': [],
'images': [],
'installable': True,
'auto_install': False,
'license': 'AGPL-3',
'application': True,
}

View File

@@ -1,120 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 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 openerp.osv.orm import TransientModel
class easy_reconcile_advanced_ref(TransientModel):
_name = 'easy.reconcile.advanced.ref'
_inherit = 'easy.reconcile.advanced'
_auto = True # False when inherited from AbstractModel
def _skip_line(self, cr, uid, rec, move_line, context=None):
"""
When True is returned on some conditions, the credit move line
will be skipped for reconciliation. Can be inherited to
skip on some conditions. ie: ref or partner_id is empty.
"""
return not (move_line.get('ref') and move_line.get('partner_id'))
def _matchers(self, cr, uid, rec, move_line, context=None):
"""
Return the values used as matchers to found the opposite lines
All the matcher keys in the dict must have their equivalent in
the `_opposite_matchers`.
The values of each matcher key will be searched in the
one returned by the `_opposite_matchers`
Must be inherited to implement the matchers for one method
As instance, it can returns:
return ('ref', move_line['rec'])
or
return (('partner_id', move_line['partner_id']),
('ref', "prefix_%s" % move_line['rec']))
All the matchers have to be found in the opposite lines
to consider them as "opposite"
The matchers will be evaluated in the same order than declared
vs the the opposite matchers, so you can gain performance by
declaring first the partners with the less computation.
All matchers should match with their opposite to be considered
as "matching".
So with the previous example, partner_id and ref have to be
equals on the opposite line matchers.
:return: tuple of tuples (key, value) where the keys are
the matchers keys
(must be the same than `_opposite_matchers` returns,
and their values to match in the opposite lines.
A matching key can have multiples values.
"""
return (('partner_id', move_line['partner_id']),
('ref', move_line['ref'].lower().strip()))
def _opposite_matchers(self, cr, uid, rec, move_line, context=None):
"""
Return the values of the opposite line used as matchers
so the line is matched
Must be inherited to implement the matchers for one method
It can be inherited to apply some formatting of fields
(strip(), lower() and so on)
This method is the counterpart of the `_matchers()` method.
Each matcher have to yield its value respecting the orders
of the `_matchers()`.
When a matcher does not correspond, the next matchers won't
be evaluated so the ones which need the less computation
have to be executed first.
If the `_matchers()` returns:
(('partner_id', move_line['partner_id']),
('ref', move_line['ref']))
Here, you should yield :
yield ('partner_id', move_line['partner_id'])
yield ('ref', move_line['ref'])
Note that a matcher can contain multiple values, as instance,
if for a move line, you want to search from its `ref` in the
`ref` or `name` fields of the opposite move lines, you have to
yield ('partner_id', move_line['partner_id'])
yield ('ref', (move_line['ref'], move_line['name'])
An OR is used between the values for the same key.
An AND is used between the differents keys.
:param dict move_line: values of the move_line
:yield: matchers as tuple ('matcher key', value(s))
"""
yield ('partner_id', move_line['partner_id'])
yield ('ref', (move_line['ref'].lower().strip(),
move_line['name'].lower().strip()))

View File

@@ -1,274 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 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 itertools import groupby, product
from operator import itemgetter
from openerp.osv.orm import Model, AbstractModel, TransientModel
from openerp.osv import fields
class easy_reconcile_advanced(AbstractModel):
_name = 'easy.reconcile.advanced'
_inherit = 'easy.reconcile.base'
def _query_debit(self, cr, uid, rec, context=None):
"""Select all move (debit>0) as candidate. Optional choice on invoice
will filter with an inner join on the related moves.
"""
select = self._select(rec)
sql_from = self._from(rec)
where, params = self._where(rec)
where += " AND account_move_line.debit > 0 "
where2, params2 = self._get_filter(cr, uid, rec, context=context)
query = ' '.join((select, sql_from, where, where2))
cr.execute(query, params + params2)
return cr.dictfetchall()
def _query_credit(self, cr, uid, rec, context=None):
"""Select all move (credit>0) as candidate. Optional choice on invoice
will filter with an inner join on the related moves.
"""
select = self._select(rec)
sql_from = self._from(rec)
where, params = self._where(rec)
where += " AND account_move_line.credit > 0 "
where2, params2 = self._get_filter(cr, uid, rec, context=context)
query = ' '.join((select, sql_from, where, where2))
cr.execute(query, params + params2)
return cr.dictfetchall()
def _matchers(self, cr, uid, rec, move_line, context=None):
"""
Return the values used as matchers to found the opposite lines
All the matcher keys in the dict must have their equivalent in
the `_opposite_matchers`.
The values of each matcher key will be searched in the
one returned by the `_opposite_matchers`
Must be inherited to implement the matchers for one method
As instance, it can returns:
return ('ref', move_line['rec'])
or
return (('partner_id', move_line['partner_id']),
('ref', "prefix_%s" % move_line['rec']))
All the matchers have to be found in the opposite lines
to consider them as "opposite"
The matchers will be evaluated in the same order than declared
vs the the opposite matchers, so you can gain performance by
declaring first the partners with the less computation.
All matchers should match with their opposite to be considered
as "matching".
So with the previous example, partner_id and ref have to be
equals on the opposite line matchers.
:return: tuple of tuples (key, value) where the keys are
the matchers keys
(must be the same than `_opposite_matchers` returns,
and their values to match in the opposite lines.
A matching key can have multiples values.
"""
raise NotImplementedError
def _opposite_matchers(self, cr, uid, rec, move_line, context=None):
"""
Return the values of the opposite line used as matchers
so the line is matched
Must be inherited to implement the matchers for one method
It can be inherited to apply some formatting of fields
(strip(), lower() and so on)
This method is the counterpart of the `_matchers()` method.
Each matcher have to yield its value respecting the orders
of the `_matchers()`.
When a matcher does not correspond, the next matchers won't
be evaluated so the ones which need the less computation
have to be executed first.
If the `_matchers()` returns:
(('partner_id', move_line['partner_id']),
('ref', move_line['ref']))
Here, you should yield :
yield ('partner_id', move_line['partner_id'])
yield ('ref', move_line['ref'])
Note that a matcher can contain multiple values, as instance,
if for a move line, you want to search from its `ref` in the
`ref` or `name` fields of the opposite move lines, you have to
yield ('partner_id', move_line['partner_id'])
yield ('ref', (move_line['ref'], move_line['name'])
An OR is used between the values for the same key.
An AND is used between the differents keys.
:param dict move_line: values of the move_line
:yield: matchers as tuple ('matcher key', value(s))
"""
raise NotImplementedError
@staticmethod
def _compare_values(key, value, opposite_value):
"""Can be inherited to modify the equality condition
specifically according to the matcher key (maybe using
a like operator instead of equality on 'ref' as instance)
"""
# consider that empty vals are not valid matchers
# it can still be inherited for some special cases
# where it would be allowed
if not (value and opposite_value):
return False
if value == opposite_value:
return True
return False
@staticmethod
def _compare_matcher_values(key, values, opposite_values):
""" Compare every values from a matcher vs an opposite matcher
and return True if it matches
"""
for value, ovalue in product(values, opposite_values):
# we do not need to compare all values, if one matches
# we are done
if easy_reconcile_advanced._compare_values(key, value, ovalue):
return True
return False
@staticmethod
def _compare_matchers(matcher, opposite_matcher):
"""
Prepare and check the matchers to compare
"""
mkey, mvalue = matcher
omkey, omvalue = opposite_matcher
assert mkey == omkey, "A matcher %s is compared with a matcher %s, " \
" the _matchers and _opposite_matchers are probably wrong" % \
(mkey, omkey)
if not isinstance(mvalue, (list, tuple)):
mvalue = mvalue,
if not isinstance(omvalue, (list, tuple)):
omvalue = omvalue,
return easy_reconcile_advanced._compare_matcher_values(mkey, mvalue, omvalue)
def _compare_opposite(self, cr, uid, rec, move_line, opposite_move_line,
matchers, context=None):
opp_matchers = self._opposite_matchers(cr, uid, rec, opposite_move_line,
context=context)
for matcher in matchers:
try:
opp_matcher = opp_matchers.next()
except StopIteration:
# if you fall here, you probably missed to put a `yield`
# in `_opposite_matchers()`
raise ValueError("Missing _opposite_matcher: %s" % matcher[0])
if not self._compare_matchers(matcher, opp_matcher):
# if any of the matcher fails, the opposite line
# is not a valid counterpart
# directly returns so the next yield of _opposite_matchers
# are not evaluated
return False
return True
def _search_opposites(self, cr, uid, rec, move_line, opposite_move_lines, context=None):
"""
Search the opposite move lines for a move line
:param dict move_line: the move line for which we search opposites
:param list opposite_move_lines: list of dict of move lines values, the move
lines we want to search for
:return: list of matching lines
"""
matchers = self._matchers(cr, uid, rec, move_line, context=context)
return [op for op in opposite_move_lines if \
self._compare_opposite(cr, uid, rec, move_line, op, matchers, context=context)]
def _action_rec(self, cr, uid, rec, context=None):
credit_lines = self._query_credit(cr, uid, rec, context=context)
debit_lines = self._query_debit(cr, uid, rec, context=context)
return self._rec_auto_lines_advanced(
cr, uid, rec, credit_lines, debit_lines, context=context)
def _skip_line(self, cr, uid, rec, move_line, context=None):
"""
When True is returned on some conditions, the credit move line
will be skipped for reconciliation. Can be inherited to
skip on some conditions. ie: ref or partner_id is empty.
"""
return False
def _rec_auto_lines_advanced(self, cr, uid, rec, credit_lines, debit_lines, context=None):
if context is None:
context = {}
reconciled_ids = []
partial_reconciled_ids = []
reconcile_groups = []
for credit_line in credit_lines:
if self._skip_line(cr, uid, rec, credit_line, context=context):
continue
opposite_lines = self._search_opposites(
cr, uid, rec, credit_line, debit_lines, context=context)
if not opposite_lines:
continue
opposite_ids = [l['id'] for l in opposite_lines]
line_ids = opposite_ids + [credit_line['id']]
for group in reconcile_groups:
if any([lid in group for lid in opposite_ids]):
group.update(line_ids)
break
else:
reconcile_groups.append(set(line_ids))
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, full = self._reconcile_lines(
cr, uid, rec, group_lines, allow_partial=True, context=context)
if reconciled and full:
reconciled_ids += reconcile_group_ids
elif reconciled:
partial_reconciled_ids += reconcile_group_ids
return reconciled_ids, partial_reconciled_ids

View File

@@ -1,37 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 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 openerp.osv.orm import Model
class account_easy_reconcile_method(Model):
_inherit = 'account.easy.reconcile.method'
def _get_all_rec_method(self, cr, uid, context=None):
methods = super(account_easy_reconcile_method, self).\
_get_all_rec_method(cr, uid, context=context)
methods += [
('easy.reconcile.advanced.ref',
'Advanced. Partner and Ref.'),
]
return methods

View File

@@ -1,20 +0,0 @@
<?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">
<group colspan="2" col="2">
<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"/>
</group>
</page>
</field>
</record>
</data>
</openerp>

View File

@@ -1,22 +1,20 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Joel Grand-Guillaume
# Copyright 2011-2012 Camptocamp SA
# Author Nicolas Bessi. Copyright Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# 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 Affero General Public License for more details.
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import statement
import statement_auto_reconcile

View File

@@ -0,0 +1,338 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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
from osv import osv, fields
from tools.translate import _
from operator import itemgetter, attrgetter
from itertools import groupby
import logging
logger = logging.getLogger('account.statement.reconcile')
class AccountsStatementAutoReconcile(osv.osv_memory):
_name = 'account.statement.import.automatic.reconcile'
_description = 'Automatic Reconcile'
_columns = {
'account_ids': fields.many2many('account.account',
'statement_reconcile_account_rel',
'reconcile_id',
'account_id',
'Accounts to Reconcile',
domain=[('reconcile', '=', True)]),
'partner_ids': fields.many2many('res.partner',
'statement_reconcile_res_partner_rel',
'reconcile_id',
'res_partner_id',
'Partners to Reconcile'),
'invoice_ids': fields.many2many('account.invoice',
'statement_account_invoice_rel',
'reconcile_id',
'invoice_id',
'Invoices to Reconcile',
domain = [('type','=','out_invoice')]),
'writeoff_acc_id': fields.many2one('account.account', 'Account'),
'writeoff_amount_limit': fields.float('Max amount allowed for write off'),
'journal_id': fields.many2one('account.journal', 'Journal'),
'reconciled': fields.integer('Reconciled transactions', readonly=True),
'allow_write_off': fields.boolean('Allow write off'),
}
def _get_reconciled(self, cr, uid, context=None):
if context is None:
context = {}
return context.get('reconciled', 0)
_defaults = {
'reconciled': _get_reconciled,
}
def return_stats(self, cr, uid, reconciled, context=None):
obj_model = self.pool.get('ir.model.data')
context = context or {}
context.update({'reconciled': reconciled})
model_data_ids = obj_model.search(
cr, uid,
[('model','=','ir.ui.view'),
('name','=','stat_account_automatic_reconcile_view1')]
)
resource_id = obj_model.read(
cr, uid, model_data_ids, fields=['res_id'])[0]['res_id']
return {
'view_type': 'form',
'view_mode': 'form',
'res_model': 'account.statement.import.automatic.reconcile',
'views': [(resource_id,'form')],
'type': 'ir.actions.act_window',
'target': 'new',
'context': context,
}
def _below_write_off_limit(self, cr, uid, lines,
writeoff_limit, context=None):
keys = ('debit', 'credit')
sums = reduce(lambda x, y:
dict((k, v + y[k]) for k, v in x.iteritems() if k in keys),
lines)
debit, credit = sums['debit'], sums['credit']
writeoff_amount = debit - credit
return bool(writeoff_limit >= abs(writeoff_amount))
def _query_moves(self, cr, uid, form, context=None):
"""Select all move (debit>0) as candidate. Optionnal choice on invoice
will filter with an inner join on the related moves.
"""
sql_params=[]
select_sql = ("SELECT "
"l.account_id, "
"l.ref as transaction_id, "
"l.name as origin, "
"l.id as invoice_id, "
"l.move_id as move_id, "
"l.id as move_line_id, "
"l.debit, l.credit, "
"l.partner_id "
"FROM account_move_line l "
"INNER JOIN account_move m "
"ON m.id = l.move_id ")
where_sql = (
"WHERE "
# "AND l.move_id NOT IN %(invoice_move_ids)s "
"l.reconcile_id IS NULL "
# "AND NOT EXISTS (select id FROM account_invoice i WHERE i.move_id = m.id) "
"AND l.debit > 0 ")
if form.account_ids:
account_ids = [str(x.id) for x in form.account_ids]
sql_params = {'account_ids': tuple(account_ids)}
where_sql += "AND l.account_id in %(account_ids)s "
if form.invoice_ids:
invoice_ids = [str(x.id) for x in form.invoice_ids]
where_sql += "AND i.id IN %(invoice_ids)s "
select_sql += "INNER JOIN account_invoice i ON m.id = i.move_id "
sql_params['invoice_ids'] = tuple(invoice_ids)
if form.partner_ids:
partner_ids = [str(x.id) for x in form.partner_ids]
where_sql += "AND l.partner_id IN %(partner_ids)s "
sql_params['partner_ids'] = tuple(partner_ids)
sql = select_sql + where_sql
cr.execute(sql, sql_params)
return cr.dictfetchall()
def _query_payments(self, cr, uid, account_id, invoice_move_ids, context=None):
sql_params = {'account_id': account_id,
'invoice_move_ids': tuple(invoice_move_ids)}
sql = ("SELECT l.id, l.move_id, "
"l.ref, l.name, "
"l.debit, l.credit, "
"l.period_id as period_id, "
"l.partner_id "
"FROM account_move_line l "
"INNER JOIN account_move m "
"ON m.id = l.move_id "
"WHERE l.account_id = %(account_id)s "
"AND l.move_id NOT IN %(invoice_move_ids)s "
"AND l.reconcile_id IS NULL "
"AND NOT EXISTS (select id FROM account_invoice i WHERE i.move_id = m.id) "
"AND l.credit > 0")
cr.execute(sql, sql_params)
return cr.dictfetchall()
@staticmethod
def _groupby_keys(keys, lines):
res = {}
key = keys.pop(0)
sorted_lines = sorted(lines, key=itemgetter(key))
for reference, iter_lines in groupby(sorted_lines, itemgetter(key)):
group_lines = list(iter_lines)
if keys:
group_lines = (AccountsStatementAutoReconcile.
_groupby_keys(keys[:], group_lines))
else:
# as we sort on all the keys, the last list
# is perforce alone in the list
group_lines = group_lines[0]
res[reference] = group_lines
return res
def _search_payment_ref(self, cr, uid, all_payments,
reference_key, reference, context=None):
def compare_key(payment, key, reference_patterns):
if not payment.get(key):
return False
if payment.get(key).lower() in reference_patterns:
return True
res = []
if not reference:
return res
lref = reference.lower()
reference_patterns = (lref, 'tid_' + lref, 'tid_mag_' + lref)
res_append = res.append
for payment in all_payments:
if (compare_key(payment, 'ref', reference_patterns) or
compare_key(payment, 'name', reference_patterns)):
res_append(payment)
# remove payment from all_payments?
# if res:
# print '----------------------------------'
# print 'ref: ' + reference
# for l in res:
# print (l.get('ref','') or '') + ' ' + (l.get('name','') or '')
return res
def _search_payments(self, cr, uid, all_payments,
references, context=None):
payments = []
for field_reference in references:
ref_key, reference = field_reference
payments = self._search_payment_ref(
cr, uid, all_payments, ref_key, reference, context=context)
# if match is found for one reference (transaction_id or origin)
# we have found our payments, don't need to search for the order
# reference
if payments:
break
return payments
def reconcile(self, cr, uid, form_id, context=None):
context = context or {}
move_line_obj = self.pool.get('account.move.line')
period_obj = self.pool.get('account.period')
if isinstance(form_id, list):
form_id = form_id[0]
form = self.browse(cr, uid, form_id)
allow_write_off = form.allow_write_off
if not form.account_ids :
raise osv.except_osv(_('UserError'),
_('You must select accounts to reconcile'))
# returns a list with a dict per line :
# [{'account_id': 5,'reference': 'A', 'move_id': 1, 'move_line_id': 1},
# {'account_id': 5,'reference': 'A', 'move_id': 1, 'move_line_id': 2},
# {'account_id': 6,'reference': 'B', 'move_id': 3, 'move_line_id': 3}],
moves = self._query_moves(cr, uid, form, context=context)
if not moves:
return False
# returns a tree :
# { 5: {1: {1: {'reference': 'A', 'move_id': 1, 'move_line_id': 1}},
# {2: {'reference': 'A', 'move_id': 1, 'move_line_id': 2}}}},
# 6: {3: {3: {'reference': 'B', 'move_id': 3, 'move_line_id': 3}}}}}
moves_tree = self._groupby_keys(['account_id',
'move_id',
'move_line_id'],
moves)
reconciled = 0
details = ""
for account_id, account_tree in moves_tree.iteritems():
# [0] because one move id per invoice
account_move_ids = [move_tree.keys() for
move_tree in account_tree.values()]
account_payments = self._query_payments(cr, uid,
account_id,
account_move_ids[0],
context=context)
for move_id, move_tree in account_tree.iteritems():
# in any case one invoice = one move
# move_id, move_tree = invoice_tree.items()[0]
move_line_ids = []
move_lines = []
move_lines_ids_append = move_line_ids.append
move_lines_append = move_lines.append
for move_line_id, vals in move_tree.iteritems():
move_lines_ids_append(move_line_id)
move_lines_append(vals)
# take the first one because the reference
# is the same everywhere for an invoice
transaction_id = move_lines[0]['transaction_id']
origin = move_lines[0]['origin']
partner_id = move_lines[0]['partner_id']
references = (('transaction_id', transaction_id),
('origin', origin))
partner_payments = [p for p in account_payments if \
p['partner_id'] == partner_id]
payments = self._search_payments(
cr, uid, partner_payments, references, context=context)
if not payments:
continue
payment_ids = [p['id'] for p in payments]
# take the period of the payment last move line
# it will be used as the reconciliation date
# and for the write off date
period_ids = [ml['period_id'] for ml in payments]
periods = period_obj.browse(
cr, uid, period_ids, context=context)
last_period = max(periods, key=attrgetter('date_stop'))
reconcile_ids = move_line_ids + payment_ids
do_write_off = (allow_write_off and
self._below_write_off_limit(
cr, uid, move_lines + payments,
form.writeoff_amount_limit,
context=context))
# date of reconciliation
rec_ctx = dict(context, date_p=last_period.date_stop)
try:
if do_write_off:
r_id = move_line_obj.reconcile(cr,
uid,
reconcile_ids,
'auto',
form.writeoff_acc_id.id,
# period of the write-off
last_period.id,
form.journal_id.id,
context=rec_ctx)
logger.info("Auto statement reconcile: Reconciled with write-off move id %s" % (move_id,))
else:
r_id = move_line_obj.reconcile_partial(cr,
uid,
reconcile_ids,
'manual',
context=rec_ctx)
logger.info("Auto statement reconcile: Partial Reconciled move id %s" % (move_id,))
except Exception, exc:
logger.error("Auto statement reconcile: Can't reconcile move id %s because: %s" % (move_id, exc,))
reconciled += 1
cr.commit()
return self.return_stats(cr, uid, reconciled, context)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="account_automatic_reconcile_view" model="ir.ui.view">
<field name="name">Account Automatic Reconcile</field>
<field name="model">account.statement.import.automatic.reconcile</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Reconciliation">
<separator string="Reconciliation" colspan="4"/>
<label colspan="4" nolabel="1" string="For an invoice to be considered as paid, the invoice entries must be reconciled with counterparts, usually bank payments. It could also be reconciled with an intermediate account of a payment office (like PayPal, Amazone, ...).
With this automatic reconciliation functionality, OpenERP makes its own search for entries to reconcile in a series of accounts. It finds entries for each transaction where the id or origin correspond."/>
<newline/>
<group>
<field name="account_ids" colspan="4" domain="[('reconcile','=',True)]"/>
<field name="partner_ids" colspan="4"/>
<field name="invoice_ids" colspan="4" domain="[('type', '=', 'out_invoice'), ('state', '=', 'open')]"/>
<field name="allow_write_off"/>
</group>
<newline/>
<group attrs="{'readonly':[('allow_write_off', '!=', True)]}">
<separator string="Write-Off Move" colspan="4"/>
<field name="writeoff_acc_id" attrs="{ 'required':[('allow_write_off', '=', True)]}"/>
<field name="writeoff_amount_limit" attrs="{ 'required':[('allow_write_off', '=', True)]}"/>
<field name="journal_id" attrs="{ 'required':[('allow_write_off', '=', True)]}"/>
</group>
<separator string ="" colspan="4"/>
<group colspan="2" col="4">
<button special="cancel" string="Cancel" icon="gtk-cancel"/>
<button name="reconcile" string="Reconcile" type="object" icon="terp-stock_effects-object-colorize"/>
</group>
</form>
</field>
</record>
<record id="action_account_automatic_reconcile" model="ir.actions.act_window">
<field name="name">Account Automatic Reconcile</field>
<field name="res_model">account.statement.import.automatic.reconcile</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="account_automatic_reconcile_view"/>
<field name="target">new</field>
</record>
<menuitem
icon="STOCK_EXECUTE"
name="Automatic Statement Reconciliation"
action="action_account_automatic_reconcile"
id="menu_automatic_reconcile"
parent="account.menu_finance_periodical_processing"
/>
<record id="stat_account_automatic_reconcile_view1" model="ir.ui.view">
<field name="name">Automatic reconcile unreconcile</field>
<field name="model">account.statement.import.automatic.reconcile</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Statement Reconciliation result">
<field name="reconciled"/>
<newline/>
<group colspan="4" col="6">
<separator colspan="6"/>
<button special="cancel" string="Ok" icon="terp-dialog-close" default_focus="1"/>
</group>
</form>
</field>
</record>
</data>
</openerp>

View File

@@ -1,23 +0,0 @@
# -*- 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
import partner

View File

@@ -1,71 +0,0 @@
# -*- 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',
'security/ir.model.access.csv',
],
'demo_xml': [],
'test': [],
'installable': True,
'images': [],
'auto_install': False,
'license': 'AGPL-3',
'active': False,
}

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<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

@@ -1,38 +0,0 @@
# -*- 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

@@ -1,22 +0,0 @@
<?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

@@ -1,3 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_bank_st_cmpl_user,account.statement.completion.rule,model_account_statement_completion_rule,account.group_account_user,1,0,0,0
access_account_bank_st_cmpl_manager,account.statement.completion.rule,model_account_statement_completion_rule,account.group_account_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_bank_st_cmpl_user account.statement.completion.rule model_account_statement_completion_rule account.group_account_user 1 0 0 0
3 access_account_bank_st_cmpl_manager account.statement.completion.rule model_account_statement_completion_rule account.group_account_manager 1 1 1 1

View File

@@ -1,388 +0,0 @@
# -*- 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
import datetime
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" (Ref:%s) was matched by more than one partner.')%(st_line.name,st_line.ref))
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" (Ref:%s) was matched by more than one partner.')%(st_line.name,st_line.ref))
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 partner_obj.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" (Ref:%s) was matched by more than one partner.')%(st_line.name,st_line.ref))
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"
pattern = ".*%s.*" % st_line.label
cursor.execute(sql, (pattern,))
result = cursor.fetchall()
if len(result) > 1:
raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more than one partner.')%(st_line.name,st_line.ref))
for id in result[0]:
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 dict value that can be passed directly to the write method of
the statement line or {}. The first dict has statement line ID as a key:
{117009: {'partner_id': 100997, 'account_id': 489L}}
"""
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, exc.value)
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"
_columns = {
'completion_logs': fields.text('Completion Log', readonly=True),
}
def write_completion_log(self, cr, uid, stat_id, error_msg, number_imported, context=None):
"""
Write the log in the completion_logs field of the bank statement to let the user
know what have been done. This is an append mode, so we don't overwrite what
already recoded.
:param int/long stat_id: ID of the account.bank.statement
:param char error_msg: Message to add
:number_imported int/long: Number of lines that have been completed
:return : True
"""
error_log = ""
user_name = self.pool.get('res.users').read(cr, uid, uid, ['name'])['name']
log = self.read(cr, uid, stat_id, ['completion_logs'], context=context)['completion_logs']
log_line = log and log.split("\n") or []
completion_date = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if error_msg:
error_log = error_msg
log_line[0:0] = [completion_date + ' : '
+ _("Bank Statement ID %s has %s lines completed by %s") %(stat_id, number_imported, user_name)
+ "\n" + error_log + "-------------" + "\n"]
log = "\n".join(log_line)
self.write(cr, uid, [stat_id], {'completion_logs' : log}, context=context)
logger.notifyChannel('Bank Statement Completion', netsvc.LOG_INFO,
"Bank Statement ID %s has %s lines completed"%(stat_id, number_imported))
return True
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 !
"""
if not context:
context={}
stat_line_obj = self.pool.get('account.bank.statement.line')
msg = ""
compl_lines = 0
for stat in self.browse(cr, uid, ids, context=context):
ctx = context.copy()
for line in stat.line_ids:
res = {}
try:
res = stat_line_obj.get_line_values_from_rules(cr, uid, [line.id], context=ctx)
if res:
compl_lines += 1
except ErrorTooManyPartner, exc:
msg += exc.value + "\n"
except Exception, exc:
msg += exc.value + "\n"
# vals = res and res.keys() or False
if res:
vals = res[line.id]
vals['already_completed'] = True
stat_line_obj.write(cr, uid, line.id, vals, context=ctx)
self.write_completion_log(cr, uid, stat.id, msg, compl_lines, context=context)
return True

View File

@@ -1,104 +0,0 @@
<?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='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>
<xpath expr="/form/notebook/page[@string='Journal Entries']" position="after">
<page string="Completion Logs" attrs="{'invisible':[('completion_logs','=',False)]}">
<field name="completion_logs" colspan="4" nolabel="1" attrs="{'invisible':[('completion_logs','=',False)]}"/>
</page>
</xpath>
</data>
</field>
</record>
<record id="bank_statement_view_form2" model="ir.ui.view">
<field name="name">account_bank_statement_import_base.bank_statement.auto_cmpl</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']/tree/field[@name='amount']" position="after">
<field name="already_completed" />
</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

@@ -1,23 +0,0 @@
# -*- 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

@@ -1,68 +0,0 @@
# -*- 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

@@ -1,4 +0,0 @@
"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

View File

@@ -1,25 +0,0 @@
# -*- 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

@@ -1,102 +0,0 @@
# -*- 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

@@ -1,213 +0,0 @@
# -*- 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

@@ -1,233 +0,0 @@
# -*- 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:
statement_obj.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

@@ -1,44 +0,0 @@
<?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_statement_base_completion.bank_statement_view_form" />
<field name="type">form</field>
<field eval="20" name="priority"/>
<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,44 +0,0 @@
# -*- 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 extension with voucher",
'version': '1.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
'depends': ['account_statement_base_completion','account_voucher'],
'description': """
This module is only needed when using account_statement_base_completion with voucher in order adapt the view correctly.
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': [
"statement_view.xml",
],
'demo_xml': [],
'test': [],
'installable': True,
'images': [],
'auto_install': True,
'license': 'AGPL-3',
'active': False,
}

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Override what we have in account_statement_base_completion to replace the one in the voucher -->
<record id="account_statement_base_completion.bank_statement_view_form2" model="ir.ui.view">
<field name="name">account_bank_statement_import_base.bank_statement.auto_cmpl</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account_voucher.view_bank_statement_tree_voucher" />
<field name="type">form</field>
<field name="arch" type="xml">
<data>
<xpath expr="/form/notebook/page/field[@name='line_ids']/tree/field[@name='voucher_id']" position="after">
<field name="already_completed" />
</xpath>
</data>
</field>
</record>
</data>
</openerp>

View File

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

View File

@@ -25,54 +25,43 @@
'maintainer': 'Camptocamp',
'category': 'Finance',
'complexity': 'normal', #easy, normal, expert
'depends': ['account'],
'depends': ['base_transaction_id'],
'description': """
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).
The goal of this module is to help dealing with huge volume of reconciliation through
payment offices like Paypal, Lazer, Visa, Amazon and so on. It's mostly used for
E-commerce but can be usefule for other use cases as it introduce a notion of profil
on the bank statement to have more control on the generated entries.
Features:
1) This module improves the bank statement 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:
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:
- The journal to use
- Account commission and partner relation
- Can force an account for the reconciliation
- Choose to use balance check or not
- 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)
- Analytic account for commission
- Force Partner on the counter-part move (e.g. 100.- debit, Partner: M.Martin; 100.- credit, Partner: HSBC)
2) Adds a report on bank statement that can be used for checks remittance
2) Adds a report on bank statement that can be used for Checks
3) When an error occurs in a bank statement confirmation, it will go through all line anyway and summarize
all the erronous line in a same popup instead of raising and crashing on every step.
4) Remove the period on the bank statement, and compute it for each line based on their date instead.
5) Cancelling a bank statement is much more easy and will cancel all related entries, unreconcile them,
and finally delete them.
5) Provide a standard import format to create and fullfill a bank statement from a .csv or .xls file. For
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',
'security/ir.model.access.csv',
],
'demo_xml': [],
'test': [],

View File

@@ -1,40 +0,0 @@
# -*- 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

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Joel Grand-Guillaume
# Author: Nicolas Bessi
# Copyright 2011-2012 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
@@ -19,3 +19,4 @@
#
##############################################################################
import parser

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Copyright Camptocamp SA
# Author Nicolas Bessi, Joel Grand-Guillaume
# Author Nicolas Bessi
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -18,95 +18,65 @@
#
##############################################################################
from openerp.tools.translate import _
"""
Parser for csv or xml for file containing credit transfert data from
financial institure as VISA
"""
from tools.translate import _
import base64
import csv
import tempfile
import datetime
from parser import BankStatementImportParser
from parser import UnicodeDictReader
try:
import xlrd
except:
raise Exception(_('Please install python lib xlrd'))
class FileParser(BankStatementImportParser):
"""
Generic abstract class for defining parser for .csv or .xls file format.
"""
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()])
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)
# TODO extract into a lib helper module
class FileParser(object):
def __init__(self, filebuffer, keys_to_validate=None, decode_base_64=True, ftype='csv'):
if ftype in ('csv', 'xls'):
self.ftype = ftype
else:
raise Exception(_('Invalide file type %s. please use csv or xls') % (ftype))
self.keys_to_validate = keys_to_validate
self.convertion_dict = convertion_dict
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
"""
self.decode_base_64 = decode_base_64
if filebuffer:
self.filebuffer = filebuffer
else:
raise Exception(_('No buffer file'))
def parse(self):
"launch parsing of csv or xls"
if self.decode_base_64:
self._decode_64b_stream()
res = None
if self.ftype == 'csv':
res = self._parse_csv()
res = self.parse_csv()
else:
res = self._parse_xls()
self.result_row_list = res
return True
res = self.parse_xls()
if self.keys_to_validate:
self._validate_column(res, self.keys_to_validate)
return res
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:
def _decode_64b_stream(self):
self.filebuffer = base64.b64decode(self.filebuffer)
return self.filebuffer
def _validate_column(self, array_of_dict, cols):
parsed_cols = array_of_dict[0].keys()
for col in cols:
if col not in parsed_cols:
raise Exception(_('Column %s not present in file') % (col))
return True
raise Exception(_('col %s not present in file') % (col))
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)
"""
def parse_csv(self, delimiter=';'):
"return an array of dict from csv file"
csv_file = tempfile.NamedTemporaryFile()
csv_file.write(self.filebuffer)
# We ensure that cursor is at beginig of file
@@ -117,10 +87,8 @@ class FileParser(BankStatementImportParser):
)
return [x for x in reader]
def _parse_xls(self):
"""
:return: dict of dict from xls file (line/rows)
"""
def parse_xls(self):
"return an array of dict from csv file"
wb_file = tempfile.NamedTemporaryFile()
wb_file.write(self.filebuffer)
# We ensure that cursor is at beginig of file
@@ -138,10 +106,6 @@ class FileParser(BankStatementImportParser):
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:
@@ -153,10 +117,6 @@ class FileParser(BankStatementImportParser):
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:
@@ -166,11 +126,6 @@ class FileParser(BankStatementImportParser):
line[rule] = conversion_rules[rule](line[rule])
return result_set
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.
"""
def cast_rows(self, result_set, conversion_rules):
func = getattr(self, '_from_%s'%(self.ftype))
res = func(self.result_row_list, self.convertion_dict)
return res
return func(result_set, conversion_rules)

View File

@@ -5,11 +5,10 @@
<report auto="False"
id="report_bank_statement_webkit"
model="account.bank.statement"
name="bank_statement_webkit"
file="account_statement_ext/report/bank_statement_report.mako"
name="account_statement_import.report_bank_statement_webkit"
file="account_statement_import/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

@@ -27,6 +27,8 @@ 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):
@@ -64,7 +66,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.bank_statement_webkit',
webkit_report.WebKitParser('report.report_bank_statement_webkit',
'account.bank.statement',
'addons/account_statement_ext/report/bank_statement_report.mako',
'addons/account_statement_import/report/bank_statement_report.mako',
parser=BankStatementWebkit)

View File

@@ -1,3 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_bank_st_profile_user,account.statement.profil,model_account_statement_profil,account.group_account_user,1,0,0,0
access_account_bank_st_profile_manager,account.statement.profil,model_account_statement_profil,account.group_account_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_bank_st_profile_user account.statement.profil model_account_statement_profil account.group_account_user 1 0 0 0
3 access_account_bank_st_profile_manager account.statement.profil model_account_statement_profil account.group_account_manager 1 1 1 1

View File

@@ -20,28 +20,22 @@
##############################################################################
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',
'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)."),
'Credit insitute partner',
help="Put a partner if you want to have it on the commission move (and optionaly\
on the counterpart of the intermediate/banking move if you tic the corresponding checkbox)."),
'journal_id': fields.many2one('account.journal',
'Financial journal to use for transaction',
required=True),
@@ -60,16 +54,13 @@ 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:
@@ -82,42 +73,43 @@ class AccountStatementProfil(Model):
class AccountBankSatement(Model):
"""
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.
"""
"""A kind of bank statement for intermediate move between customer and real bank, used
for manageing check, payment office like paypal or marketplace like amazon.
We inherit account.bank.statement because it's a very close object with only some
difference. But we want some method to be completely different, so we create a new object."""
_inherit = "account.bank.statement"
_columns = {
'profile_id': fields.many2one('account.statement.profil',
'Profil', required=True, readonly=True, states={'draft': [('readonly', False)]}),
'import_config_id': fields.many2one('account.statement.profil',
'Profil', required=True, states={'draft': [('readonly', False)]}),
'credit_partner_id': fields.related(
'profile_id',
'import_config_id',
'partner_id',
type='many2one',
relation='res.partner',
string='Financial Partner',
store=True, readonly=True),
'balance_check': fields.related(
'profile_id',
'import_config_id',
'balance_check',
type='boolean',
string='Balance check',
store=True, readonly=True),
'journal_id': fields.related(
'profile_id',
'import_config_id',
'journal_id',
type='many2one',
relation='account.journal',
string='Journal',
store=True, readonly=True),
# 'line_ids': fields.one2many('account.bank.statement.line',
# 'statement_id', 'Statement lines',
# states={'confirm':[('readonly', True)]}),
# 'move_line_ids': fields.one2many('account.move.line', 'statement_treasury_id',
# 'Entry lines', states={'confirm':[('readonly',True)]}),
# Redefine this field to avoid his computation (it is a function field on bank statement)
# 'balance_end': fields.dummy(string="Computed Balance"),
'period_id': fields.many2one('account.period', 'Period', required=False, readonly=True),
}
@@ -127,17 +119,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
need it."""
if 'profile_id' in vals:
that need it."""
if 'import_config_id' in vals:
profil_obj = self.pool.get('account.statement.profil')
profile = profil_obj.browse(cr,uid,vals['profile_id'],context)
profile = profil_obj.browse(cr,uid,vals['import_config_id'],context)
vals['journal_id'] = profile.journal_id.id
return super(AccountBankSatement, self).create(cr, uid, vals, context=context)
def _get_period(self, cursor, uid, date, context=None):
"""
Find matching period for date, used in the statement line creation.
"""
'''
Find matching period for date, used in thestatement 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
@@ -157,46 +149,24 @@ 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.
We change the move line generated from the lines depending on the profil:
Point to account.bank.statement.line instead of account.bank.statement.line.
In Treasury Statement, unlike the Bank statement, we will change the move line generated from the
lines depending on the profil (config import):
- If receivable_account_id is set, we'll use it instead of the "partner" one
- If partner_id is set, we'll us it for the commission (when imported throufh the wizard)
- If partner_id is set and force_partner_on_bank is ticked, we'll let the partner of each line
for the debit line, but we'll change it on the credit move line for the choosen partner_id
=> This will ease the reconciliation process with the bank as the partner will match the bank
=> This will ease the reconsiliation 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 = {}
@@ -276,8 +246,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.profile_id.force_partner_on_bank: # Chg
bank_parrtner_id = st.profile_id.partner_id.id # Chg
if st.import_config_id.force_partner_on_bank: # Chg
bank_parrtner_id = st.import_config_id.partner_id.id # Chg
else: # Chg
bank_parrtner_id = ((st_line.partner_id) and st_line.partner_id.id) or False # Chg
@@ -310,40 +280,24 @@ class AccountBankSatement(Model):
account_move_obj.post(cr, uid, [move_id], context=context)
return move_id
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)
"""
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"""
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
"""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 !
"""
stack + managing period from date."""
# obj_seq = self.pool.get('irerrors_stack.sequence')
if context is None:
context = {}
@@ -364,7 +318,13 @@ class AccountBankSatement(Model):
st_number = st.name
else:
# Begin Changes
st_number = self._get_st_number_period_profil(cr, uid, st.date, st.profile_id.id)
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)
# End Changes
for line in st.move_line_ids:
if line.state <> 'valid':
@@ -399,43 +359,29 @@ 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_account_for_counterpart(self, cursor, uid,
amount, account_receivable, account_payable):
"""
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
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:
account_id = account_payable
if not account_id:
raise osv.except_osv(
_('Can not determine account'),
_('Please ensure that minimal properties are set')
)
return account_id
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_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.
"""
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')
@@ -464,31 +410,48 @@ class AccountBankSatement(Model):
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 and do nothing, otherwise call super.
def _get_account_id(self, cursor, uid,
amount, account_receivable, account_payable):
"return the default account to be used by statement line"
account_id = False
if amount >= 0:
account_id = account_receivable
else:
account_id = account_payable
if not account_id:
raise osv.except_osv(
_('Can not determine account'),
_('Please ensure that minimal properties are set')
)
return account_id
:param int/long st_id: ID of the concerned account.bank.statement
:param char: journal_type that concern the bank statement
:return: True
"""
def balance_check(self, cr, uid, st_id, journal_type='bank', context=None):
"""Balance check depends on the profil. If no check for this profil is required,
return True"""
st = self.browse(cr, uid, st_id, context=context)
if st.balance_check:
return super(AccountBankSatement,self).balance_check(cr, uid, st_id, journal_type, context)
else:
return True
def onchange_imp_config_id(self, cr, uid, ids, profile_id, context=None):
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)
"""
Compute values on the change of the profile.
# 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
:param: int/long: profile_id that changed
:return dict of dict with key = name of the field
"""
if not profile_id:
def onchange_imp_config_id(self, cr, uid, ids, import_config_id, context=None):
if not import_config_id:
return {}
import_config = self.pool.get("account.statement.profil").browse(cr,uid,profile_id)
import_config = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id)
journal_id = import_config.journal_id.id
account_id = import_config.journal_id.default_debit_account_id.id
credit_partner_id = import_config.partner_id and import_config.partner_id.id or False
@@ -497,106 +460,166 @@ 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
def _get_default_account(self, cursor, user, context=None):
return self.get_values_for_line(cursor, user, context = context)['account_id']
_columns = {
# Set them as required + 64 char instead of 32
'ref': fields.char('Reference', size=64, required=True),
# 'statement_id': fields.many2one('account.bank.statement', 'Statement',
# select=True, required=True, ondelete='cascade'),
# 'move_ids': fields.many2many('account.move',
# 'account_treasury_statement_line_move_rel', 'statement_line_id','move_id',
# 'Moves'),
'ref': fields.char('Reference', size=32, required=True),
'period_id': fields.many2one('account.period', 'Period', required=True),
}
_defaults = {
'period_id': _get_period,
'account_id': _get_default_account,
}
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.
"""
# WARNING => Crash cause the super method here calls onchange_type => and then
# we don't call it from the good model.... => We'll need to override the complete method here
def onchange_partner_id(self, cr, uid, ids, partner_id, import_config_id, context=None):
# import pdb;pdb.set_trace()
# if context is None:
# context = {}
# res = super(AccountTreasurySatementLine,self).onchange_partner_id(cr, uid, ids, partner_id, context)
# c = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id)
# acc_id=c.receivable_account_id and c.receivable_account_id.id or False
# if acc_id:
# res['value'].update({'account_id':acc_id})
# return res
obj_partner = self.pool.get('res.partner')
if context is None:
context = {}
@@ -612,23 +635,34 @@ class AccountBankSatementLine(Model):
type = 'supplier'
if part.customer == True:
type = 'customer'
res_type = self.onchange_type(cr, uid, ids, partner_id, type, profile_id, context=context) # Chg
res_type = self.onchange_type(cr, uid, ids, partner_id, type, import_config_id, context=context)
if res_type['value'] and res_type['value'].get('account_id', False):
return {'value': {'type': type, 'account_id': res_type['value']['account_id']}}
return {'value': {'type': type}}
res = {'value': {'type': type, 'account_id': res_type['value']['account_id']}}
else:
res = {'value': {'type': type}}
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.
"""
c = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id)
acc_id=c.receivable_account_id and c.receivable_account_id.id or False
if acc_id:
res['value'].update({'account_id':acc_id})
return res
# TOFIX
def onchange_type(self, cr, uid, line_id, partner_id, type, import_config_id, context=None):
if context is None:
context = {}
res = super(AccountBankSatementLine,self).onchange_type(cr, uid, line_id, partner_id, type, context)
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']})
c = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id)
acc_id=c.receivable_account_id and c.receivable_account_id.id or False
if acc_id:
res['value'].update({'account_id':acc_id})
return res
# class AccountMoveLine(Model):
# _inherit = "account.move.line"
#
# _columns = {
# 'statement_treasury_id': fields.many2one('account.bank.statement', 'Statement', help="The intermediate statement used for reconciliation", select=1),
# }

View File

@@ -15,14 +15,12 @@
</field>
</record>
<record id="view_account_move_line_filter" model="ir.ui.view">
<field name="name">account.move.line.search.add_field</field>
<field name="model">account.move.line</field>
<field name="type">search</field>
<field name="inherit_id" ref="account.view_account_move_line_filter"/>
<field name="arch" type="xml">
<field name="period_id" context="{'period_id':self}" position="after">
<field name="statement_id"/>
<field name="ref"/>
</field>
</field>
</record>
@@ -42,7 +40,6 @@
<field name="receivable_account_id" />
<field name="force_partner_on_bank"/>
<field name="balance_check"/>
<field name="bank_statement_prefix"/>
</form>
</field>
</record>
@@ -93,8 +90,7 @@
<field name="type">search</field>
<field name="arch" type="xml">
<xpath expr="/search/group/field[@name='name']" position="before">
<field name="id"/>
<field name="profile_id"/>
<field name="import_config_id"/>
<field name="credit_partner_id"/>
<separator orientation="vertical"/>
</xpath>
@@ -112,11 +108,8 @@
<field name="inherit_id" ref="account.view_bank_statement_tree"/>
<field name="type">tree</field>
<field name="arch" type="xml">
<xpath expr="/tree/field[@name='name']" position="before">
<field name="id"/>
</xpath>
<xpath expr="/tree/field[@name='name']" position="after">
<field name="profile_id"/>
<field name="import_config_id"/>
</xpath>
<xpath expr="/tree/field[@name='period_id']" position="replace">
<field name="credit_partner_id"/>
@@ -138,12 +131,12 @@
</xpath>
<!-- Add a new group before the first one with name, profil and date -->
<xpath expr="/form/group[@col='7']" position="before">
<group col="8" colspan="4">
<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 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="date" select="1" on_change="onchange_date(date, company_id)"/>
</group>
<separator string="Profile Details" colspan="4"/>
<separator string="Details" colspan="4"/>
</xpath>
<!-- Make balance visible or not depending on profil -->
<xpath expr="/form/group/field[@name='balance_start']" position="replace">
@@ -158,7 +151,7 @@
</group>
</xpath>
<xpath expr="/form/group/field[@name='balance_end']" position="replace">
<field name="balance_end"/>
<field name="balance_end" attrs="{'invisible':[('balance_check','=',False)]}"/>
</xpath>
<xpath expr="/form/group/field[@name='journal_id']" position="attributes">
@@ -173,36 +166,53 @@
<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.profile_id)"/>
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.import_config_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.profile_id)"/>
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.import_config_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.profile_id)"/>
<field name="partner_id" on_change="onchange_partner_id(partner_id,parent.import_config_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.profile_id)"/>
<field name="type" on_change="onchange_type(partner_id, type, parent.import_config_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.profile_id)"/>
<field name="type" on_change="onchange_type(partner_id, type, parent.import_config_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

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author Nicolas Bessi, Joel Grand-Guillaume. Copyright Camptocamp SA
# Author Nicolas Bessi. Copyright Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by

View File

@@ -28,25 +28,15 @@ from tools.translate import _
import os
class CreditPartnerStatementImporter(osv.osv_memory):
"""Import Credit statement"""
_name = "credit.statement.import"
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
_description = __doc__
_columns = {
'profile_id': fields.many2one('account.statement.profil',
'import_config_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',
),
@@ -71,12 +61,13 @@ 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_profile_id(self, cr, uid, ids, profile_id, context=None):
def onchange_import_config_id(self, cr, uid, ids, import_config_id, context=None):
res={}
if profile_id:
c = self.pool.get("account.statement.profil").browse(cr,uid,profile_id)
if import_config_id:
c = self.pool.get("account.statement.profil").browse(cr,uid,import_config_id)
res = {'value': {'partner_id': c.partner_id and c.partner_id.id or False,
'journal_id': c.journal_id and c.journal_id.id or False, 'commission_account_id': \
c.commission_account_id and c.commission_account_id.id or False,
@@ -86,35 +77,31 @@ 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)
ftype = self._check_extension(importer.file_name)
(shortname, ftype) = os.path.splitext(importer.file_name)
if not ftype:
#We do not use osv exception we do not want to have it logged
raise Exception(_('Please use a file with an extention'))
sid = self.pool.get(
'account.statement.profil').statement_import(
'account.bank.statement').credit_statement_import(
cursor,
uid,
False,
importer.profile_id.id,
importer.import_config_id.id,
importer.input_statement,
ftype.replace('.',''),
context=context
)
return {
'domain': "[('id','in', ["+','.join(map(str,[sid]))+"])]",
'name': 'Imported Bank Statement',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'account.bank.statement',
'type': 'ir.actions.act_window',
}
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

View File

@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<form string="Import statement">
<group colspan="4" >
<field name="profile_id" on_change="onchange_profile_id(profile_id)"/>
<field name="import_config_id" on_change="onchange_import_config_id(import_config_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 Bank Statement" action="statement_importer_action" parent="account.menu_finance_bank_and_cash"/>
<menuitem id="statement_importer_menu" name="Import Treasury Statement" action="statement_importer_action" parent="account.menu_finance_periodical_processing"/>
</data>
</openerp>

View File

@@ -32,9 +32,7 @@
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': [
"statement_voucher_view.xml",
],
'update_xml': [],
'demo_xml': [],
'test': [],
'installable': True,

View File

@@ -26,25 +26,16 @@ class AccountVoucher(Model):
_inherit = 'account.voucher'
def _get_period(self, cr, uid, context=None):
"""If period not in context, take it from the move lines"""
if context is None:
context = {}
"""If perdiod not in context, take it from the move lines"""
if context is None: context = {}
if not context.get('period_id') and context.get('move_line_ids'):
res = self.pool.get('account.move.line').browse(cr, uid , context.get('move_line_ids'))[0].period_id.id
context['period_id'] = res
elif context.get('date'):
periods = self.pool.get('account.period').find(
cr, uid, dt=context['date'], context=context)
if periods:
context['period_id'] = periods[0]
return super(AccountVoucher, self)._get_period(cr, uid, context)
def create(self, cr, uid, values, context=None):
"""If no period defined in values, ask it from moves."""
if context is None:
context = {}
if not values.get('period_id'):
ctx = dict(context, date=values.get('date'))
values['period_id'] = self._get_period(cr, uid, ctx)
if values.get('period_id') == False and context.get('move_line_ids'):
values['period_id'] = self._get_period(cr, uid, context)
return super(AccountVoucher, self).create(cr, uid, values, context)

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="account_voucher.view_bank_statement_form_invoice" model="ir.ui.view">
<field name="name">account.bank.statement.invoice.form.inherit</field>
<field name="model">account.bank.statement</field>
<field name="type">form</field>
<field name="inherit_id" ref="account.view_bank_statement_form"/>
<field name="arch" type="xml">
<field name="currency" invisible="1" position="after">
</field>
</field>
</record>
</data>
</openerp>

View File

@@ -1,52 +0,0 @@
# -*- 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

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<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">30</field>
<field name="function_to_call">get_from_transaction_id_and_so</field>
</record>
</data>
</openerp>

View File

@@ -1,90 +0,0 @@
# -*- 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(cr, uid, [('transaction_id', '=', st_line.transaction_id)])
if so_id and len(so_id) == 1:
so = so_obj.browse(cr, 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" (Ref:%s) was matched by more than one partner.')%(st_line.name,st_line.ref))
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

@@ -1,22 +0,0 @@
<?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="20" 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,22 +0,0 @@
# -*- 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 statement

View File

@@ -1,60 +0,0 @@
# -*- 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

@@ -1,24 +0,0 @@
# -*- 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

@@ -1,99 +0,0 @@
# -*- 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(TransactionIDFileParser, 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

@@ -1,43 +0,0 @@
# -*- 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 openerp.osv.orm import Model, fields
from openerp.osv import fields, osv
class AccountStatementProfil(Model):
_inherit = "account.statement.profil"
def get_import_type_selection(self, cr, uid, context=None):
"""
Has to be inherited to add parser
"""
res = super(AccountStatementProfil, self).get_import_type_selection(cr, uid, context=context)
res.append(('generic_csvxls_transaction','Generic .csv/.xls based on SO transaction ID'))
return res
_columns = {
'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."),
}