mirror of
https://github.com/OCA/account-reconcile.git
synced 2025-01-20 12:27:39 +02:00
[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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
338
account_advanced_reconcile/wizard/statement_auto_reconcile.py
Normal file
338
account_advanced_reconcile/wizard/statement_auto_reconcile.py
Normal 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:
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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,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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"
|
||||
|
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -19,6 +19,7 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import file_parser
|
||||
import wizard
|
||||
import statement
|
||||
import report
|
||||
import account
|
||||
@@ -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': [],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -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)
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
@@ -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),
|
||||
# }
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -32,9 +32,7 @@
|
||||
""",
|
||||
'website': 'http://www.camptocamp.com',
|
||||
'init_xml': [],
|
||||
'update_xml': [
|
||||
"statement_voucher_view.xml",
|
||||
],
|
||||
'update_xml': [],
|
||||
'demo_xml': [],
|
||||
'test': [],
|
||||
'installable': True,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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."),
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user