mirror of
https://github.com/OCA/bank-payment.git
synced 2025-02-02 10:37:31 +02:00
Initial adjustments for 8.0 repository
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
|
||||
# All Rights Reserved
|
||||
#
|
||||
# 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 . import bank_import
|
||||
from . import banking_transaction_wizard
|
||||
from . import link_partner
|
||||
@@ -1,451 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
|
||||
# All Rights Reserved
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
##############################################################################
|
||||
# Many thanks to our contributors
|
||||
#
|
||||
# Kaspars Vilkens (KNdati): lenghty discussions, bugreports and bugfixes
|
||||
# Stefan Rijnhart (Therp): bugreport and bugfix
|
||||
#
|
||||
|
||||
'''
|
||||
This module contains the business logic of the wizard account_banking_import.
|
||||
The parsing is done in the parser modules. Every parser module is required to
|
||||
use parser.models as a mean of communication with the business logic.
|
||||
'''
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
from openerp.osv import orm, fields
|
||||
from openerp.tools.translate import _
|
||||
from openerp.addons.account_banking.parsers import models
|
||||
from openerp.addons.account_banking.parsers import convert
|
||||
from openerp.addons.account_banking.struct import struct
|
||||
from openerp.addons.account_banking.wizard import banktools
|
||||
from openerp.addons.decimal_precision import decimal_precision as dp
|
||||
|
||||
bt = models.mem_bank_transaction
|
||||
|
||||
# This variable is used to match supplier invoices with an invoice date after
|
||||
# the real payment date. This can occur with online transactions (web shops).
|
||||
payment_window = datetime.timedelta(days=10)
|
||||
|
||||
|
||||
def parser_types(*args, **kwargs):
|
||||
'''Delay evaluation of parser types until start of wizard, to allow
|
||||
depending modules to initialize and add their parsers to the list
|
||||
'''
|
||||
return models.parser_type.get_parser_types()
|
||||
|
||||
|
||||
class banking_import_line(orm.TransientModel):
|
||||
_name = 'banking.import.line'
|
||||
_description = 'Bank import lines'
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=64),
|
||||
'date': fields.date('Date', readonly=True),
|
||||
'amount': fields.float(
|
||||
'Amount',
|
||||
digits_compute=dp.get_precision('Account'),
|
||||
),
|
||||
'statement_line_id': fields.many2one(
|
||||
'account.bank.statement.line',
|
||||
'Resulting statement line', readonly=True),
|
||||
'type': fields.selection([
|
||||
('supplier', 'Supplier'),
|
||||
('customer', 'Customer'),
|
||||
('general', 'General')
|
||||
], 'Type', required=True),
|
||||
'partner_id': fields.many2one('res.partner', 'Partner'),
|
||||
'statement_id': fields.many2one(
|
||||
'account.bank.statement',
|
||||
'Statement',
|
||||
select=True,
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
),
|
||||
'ref': fields.char('Reference', size=32),
|
||||
'note': fields.text('Notes'),
|
||||
'period_id': fields.many2one('account.period', 'Period'),
|
||||
'currency': fields.many2one('res.currency', 'Currency'),
|
||||
'banking_import_id': fields.many2one(
|
||||
'account.banking.bank.import', 'Bank import',
|
||||
readonly=True, ondelete='cascade'),
|
||||
'reconcile_id': fields.many2one(
|
||||
'account.move.reconcile', 'Reconciliaton'),
|
||||
'account_id': fields.many2one('account.account', 'Account'),
|
||||
'invoice_ids': fields.many2many(
|
||||
'account.invoice', 'banking_import_line_invoice_rel',
|
||||
'line_id', 'invoice_id'),
|
||||
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account'),
|
||||
'transaction_type': fields.selection(
|
||||
[
|
||||
# TODO: payment terminal etc...
|
||||
('invoice', 'Invoice payment'),
|
||||
('storno', 'Canceled debit order'),
|
||||
('bank_costs', 'Bank costs'),
|
||||
('unknown', 'Unknown'),
|
||||
],
|
||||
'Transaction type',
|
||||
),
|
||||
'duplicate': fields.boolean('Duplicate'),
|
||||
}
|
||||
|
||||
|
||||
class banking_import(orm.TransientModel):
|
||||
_name = 'account.banking.bank.import'
|
||||
|
||||
def import_statements_file(self, cr, uid, ids, context):
|
||||
'''
|
||||
Import bank statements / bank transactions file.
|
||||
This method is a wrapper for the business logic on the transaction.
|
||||
The parser modules represent the decoding logic.
|
||||
'''
|
||||
banking_import = self.browse(cr, uid, ids, context)[0]
|
||||
statements_file = banking_import.file
|
||||
data = base64.decodestring(statements_file)
|
||||
|
||||
user_obj = self.pool.get('res.user')
|
||||
statement_obj = self.pool.get('account.bank.statement')
|
||||
statement_file_obj = self.pool.get('account.banking.imported.file')
|
||||
import_transaction_obj = self.pool.get('banking.import.transaction')
|
||||
period_obj = self.pool.get('account.period')
|
||||
|
||||
# get the parser to parse the file
|
||||
parser_code = banking_import.parser
|
||||
parser = models.create_parser(parser_code)
|
||||
if not parser:
|
||||
raise orm.except_orm(
|
||||
_('ERROR!'),
|
||||
_('Unable to import parser %(parser)s. Parser class not '
|
||||
'found.') %
|
||||
{'parser': parser_code}
|
||||
)
|
||||
|
||||
# Get the company
|
||||
company = (banking_import.company or
|
||||
user_obj.browse(cr, uid, uid, context).company_id)
|
||||
|
||||
# Parse the file
|
||||
statements = parser.parse(cr, data)
|
||||
|
||||
if any([x for x in statements if not x.is_valid()]):
|
||||
raise orm.except_orm(
|
||||
_('ERROR!'),
|
||||
_('The imported statements appear to be invalid! Check your '
|
||||
'file.')
|
||||
)
|
||||
|
||||
# Create the file now, as the statements need to be linked to it
|
||||
import_id = statement_file_obj.create(cr, uid, dict(
|
||||
company_id=company.id,
|
||||
file=statements_file,
|
||||
file_name=banking_import.file_name,
|
||||
state='unfinished',
|
||||
format=parser.name,
|
||||
))
|
||||
|
||||
bank_country_code = False
|
||||
if hasattr(parser, 'country_code'):
|
||||
bank_country_code = parser.country_code
|
||||
|
||||
# Results
|
||||
results = struct(
|
||||
stat_loaded_cnt=0,
|
||||
trans_loaded_cnt=0,
|
||||
stat_skipped_cnt=0,
|
||||
trans_skipped_cnt=0,
|
||||
trans_matched_cnt=0,
|
||||
bank_costs_invoice_cnt=0,
|
||||
error_cnt=0,
|
||||
log=[],
|
||||
)
|
||||
|
||||
# Caching
|
||||
error_accounts = {}
|
||||
info = {}
|
||||
imported_statement_ids = []
|
||||
|
||||
transaction_ids = []
|
||||
for statement in statements:
|
||||
if statement.local_account in error_accounts:
|
||||
# Don't repeat messages
|
||||
results.stat_skipped_cnt += 1
|
||||
results.trans_skipped_cnt += len(statement.transactions)
|
||||
continue
|
||||
|
||||
# Create fallback currency code
|
||||
currency_code = (
|
||||
statement.local_currency or company.currency_id.name
|
||||
)
|
||||
|
||||
# Check cache for account info/currency
|
||||
if statement.local_account in info and \
|
||||
currency_code in info[statement.local_account]:
|
||||
account_info = info[statement.local_account][currency_code]
|
||||
|
||||
else:
|
||||
# Pull account info/currency
|
||||
account_info = banktools.get_company_bank_account(
|
||||
self.pool, cr, uid, statement.local_account,
|
||||
statement.local_currency, company, results.log
|
||||
)
|
||||
if not account_info:
|
||||
results.log.append(
|
||||
_('Statements found for unknown account '
|
||||
'%(bank_account)s') % {
|
||||
'bank_account': statement.local_account
|
||||
}
|
||||
)
|
||||
error_accounts[statement.local_account] = True
|
||||
results.error_cnt += 1
|
||||
continue
|
||||
if 'journal_id' not in account_info.keys():
|
||||
results.log.append(
|
||||
_('Statements found for account %(bank_account)s, '
|
||||
'but no default journal was defined.'
|
||||
) % {'bank_account': statement.local_account}
|
||||
)
|
||||
error_accounts[statement.local_account] = True
|
||||
results.error_cnt += 1
|
||||
continue
|
||||
|
||||
# Get required currency code
|
||||
currency_code = account_info.currency_id.name
|
||||
|
||||
# Cache results
|
||||
if statement.local_account not in info:
|
||||
info[statement.local_account] = {
|
||||
currency_code: account_info
|
||||
}
|
||||
else:
|
||||
info[statement.local_account][currency_code] = account_info
|
||||
|
||||
# Final check: no coercion of currencies!
|
||||
if statement.local_currency \
|
||||
and account_info.currency_id.name != statement.local_currency:
|
||||
# TODO: convert currencies?
|
||||
results.log.append(
|
||||
_('Statement %(statement_id)s for account %(bank_account)s'
|
||||
' uses different currency than the defined bank journal.'
|
||||
) % {
|
||||
'bank_account': statement.local_account,
|
||||
'statement_id': statement.id
|
||||
}
|
||||
)
|
||||
error_accounts[statement.local_account] = True
|
||||
results.error_cnt += 1
|
||||
continue
|
||||
|
||||
# Check existence of previous statement
|
||||
# Less well defined formats can resort to a
|
||||
# dynamically generated statement identification
|
||||
# (e.g. a datetime string of the moment of import)
|
||||
# and have potential duplicates flagged by the
|
||||
# matching procedure
|
||||
statement_ids = statement_obj.search(cr, uid, [
|
||||
('name', '=', statement.id),
|
||||
('date', '=', convert.date2str(statement.date)),
|
||||
])
|
||||
if statement_ids:
|
||||
results.log.append(
|
||||
_('Statement %(id)s known - skipped') % {
|
||||
'id': statement.id
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Get the period for the statement (as bank statement object
|
||||
# checks this)
|
||||
period_ids = period_obj.search(
|
||||
cr, uid, [
|
||||
('company_id', '=', company.id),
|
||||
('date_start', '<=', statement.date),
|
||||
('date_stop', '>=', statement.date),
|
||||
('special', '=', False),
|
||||
], context=context)
|
||||
|
||||
if not period_ids:
|
||||
results.log.append(
|
||||
_('No period found covering statement date %(date)s, '
|
||||
'statement %(id)s skipped') % {
|
||||
'date': statement.date,
|
||||
'id': statement.id,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Create the bank statement record
|
||||
statement_id = statement_obj.create(cr, uid, dict(
|
||||
name=statement.id,
|
||||
journal_id=account_info.journal_id.id,
|
||||
date=convert.date2str(statement.date),
|
||||
balance_start=statement.start_balance,
|
||||
balance_end_real=statement.end_balance,
|
||||
balance_end=statement.end_balance,
|
||||
state='draft',
|
||||
user_id=uid,
|
||||
banking_id=import_id,
|
||||
company_id=company.id,
|
||||
period_id=period_ids[0],
|
||||
))
|
||||
imported_statement_ids.append(statement_id)
|
||||
|
||||
subno = 0
|
||||
for transaction in statement.transactions:
|
||||
subno += 1
|
||||
if not transaction.id:
|
||||
transaction.id = str(subno)
|
||||
values = {}
|
||||
for attr in transaction.__slots__ + ['type']:
|
||||
if attr in import_transaction_obj.column_map:
|
||||
values[import_transaction_obj.column_map[attr]] = \
|
||||
eval('transaction.%s' % attr)
|
||||
elif attr in import_transaction_obj._columns:
|
||||
values[attr] = eval('transaction.%s' % attr)
|
||||
values['statement_id'] = statement_id
|
||||
values['bank_country_code'] = bank_country_code
|
||||
values['local_account'] = statement.local_account
|
||||
values['local_currency'] = statement.local_currency
|
||||
|
||||
transaction_id = import_transaction_obj.create(
|
||||
cr, uid, values, context=context)
|
||||
transaction_ids.append(transaction_id)
|
||||
|
||||
results.stat_loaded_cnt += 1
|
||||
|
||||
import_transaction_obj.match(
|
||||
cr, uid, transaction_ids, results=results, context=context
|
||||
)
|
||||
|
||||
# recompute statement end_balance for validation
|
||||
statement_obj.button_dummy(
|
||||
cr, uid, imported_statement_ids, context=context)
|
||||
|
||||
report = [
|
||||
'%s: %s' % (_('Total number of statements'),
|
||||
results.stat_skipped_cnt + results.stat_loaded_cnt),
|
||||
'%s: %s' % (_('Total number of transactions'),
|
||||
results.trans_skipped_cnt + results.trans_loaded_cnt),
|
||||
'%s: %s' % (_('Number of errors found'),
|
||||
results.error_cnt),
|
||||
'%s: %s' % (_('Number of statements skipped due to errors'),
|
||||
results.stat_skipped_cnt),
|
||||
'%s: %s' % (_('Number of transactions skipped due to errors'),
|
||||
results.trans_skipped_cnt),
|
||||
'%s: %s' % (_('Number of statements loaded'),
|
||||
results.stat_loaded_cnt),
|
||||
'%s: %s' % (_('Number of transactions loaded'),
|
||||
results.trans_loaded_cnt),
|
||||
'%s: %s' % (_('Number of transactions matched'),
|
||||
results.trans_matched_cnt),
|
||||
'%s: %s' % (_('Number of bank costs invoices created'),
|
||||
results.bank_costs_invoice_cnt),
|
||||
'',
|
||||
'%s:' % (_('Error report')),
|
||||
'',
|
||||
]
|
||||
text_log = '\n'.join(report + results.log)
|
||||
state = results.error_cnt and 'error' or 'ready'
|
||||
statement_file_obj.write(cr, uid, import_id, dict(
|
||||
state=state,
|
||||
log=text_log,
|
||||
), context)
|
||||
if not imported_statement_ids or not results.trans_loaded_cnt:
|
||||
# file state can be 'ready' while import state is 'error'
|
||||
state = 'error'
|
||||
self.write(cr, uid, [ids[0]], dict(
|
||||
import_id=import_id,
|
||||
log=text_log,
|
||||
state=state,
|
||||
statement_ids=[(6, 0, imported_statement_ids)],
|
||||
), context)
|
||||
return {
|
||||
'name': (state == 'ready' and _('Review Bank Statements') or
|
||||
_('Error')),
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'view_id': False,
|
||||
'res_model': self._name,
|
||||
'domain': [],
|
||||
'context': dict(context, active_ids=ids),
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'res_id': ids[0] or False,
|
||||
}
|
||||
|
||||
_columns = {
|
||||
'company': fields.many2one(
|
||||
'res.company', 'Company', required=True,
|
||||
states={
|
||||
'ready': [('readonly', True)],
|
||||
'error': [('readonly', True)],
|
||||
},
|
||||
),
|
||||
'file_name': fields.char('File name', size=256),
|
||||
'file': fields.binary(
|
||||
'Statements File',
|
||||
required=True,
|
||||
help=('The Transactions File to import. Please note that while it '
|
||||
'is perfectly safe to reload the same file multiple times '
|
||||
'or to load in timeframe overlapping statements files, '
|
||||
'there are formats that may introduce different '
|
||||
'sequencing, which may create double entries.\n\n'
|
||||
'To stay on the safe side, always load bank statements '
|
||||
'files using the same format.'),
|
||||
states={
|
||||
'ready': [('readonly', True)],
|
||||
'error': [('readonly', True)],
|
||||
},
|
||||
),
|
||||
'parser': fields.selection(
|
||||
parser_types, 'File Format', required=True,
|
||||
states={
|
||||
'ready': [('readonly', True)],
|
||||
'error': [('readonly', True)],
|
||||
},
|
||||
),
|
||||
'log': fields.text('Log', readonly=True),
|
||||
'state': fields.selection(
|
||||
[('init', 'init'),
|
||||
('ready', 'ready'),
|
||||
('error', 'error')
|
||||
],
|
||||
'State', readonly=True),
|
||||
'import_id': fields.many2one(
|
||||
'account.banking.imported.file', 'Import File'),
|
||||
'statement_ids': fields.many2many(
|
||||
'account.bank.statement', 'rel_wiz_statements', 'wizard_id',
|
||||
'statement_id', 'Imported Bank Statements'),
|
||||
'line_ids': fields.one2many(
|
||||
'banking.import.line', 'banking_import_id', 'Transactions',
|
||||
),
|
||||
}
|
||||
|
||||
def _default_parser_type(self, cr, uid, context=None):
|
||||
types = models.parser_type.get_parser_types()
|
||||
return types and types[0][0] or False
|
||||
|
||||
_defaults = {
|
||||
'state': 'init',
|
||||
'company': lambda s, cr, uid, c:
|
||||
s.pool.get('res.company')._company_default_get(
|
||||
cr, uid, 'bank.import.transaction', context=c),
|
||||
'parser': _default_parser_type,
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="view_banking_import" model="ir.ui.view">
|
||||
<field name="name">account.banking.bank.import</field>
|
||||
<field name="model">account.banking.bank.import</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Import Bank Transactions File" version="7.0">
|
||||
<group colspan="4" states="init,ready,error">
|
||||
<separator colspan="4" string="Select the processing details:"/>
|
||||
<field name="company" colspan="1"/>
|
||||
<field name="file" filename="file_name"/>
|
||||
<field name="file_name" invisible="1"/>
|
||||
<newline />
|
||||
<field name="parser"/>
|
||||
<field name="state" invisible="1"/>
|
||||
</group>
|
||||
<notebook colspan="4">
|
||||
<page attrs="{'invisible': [('state', '!=', 'review')]}" string="Transactions">
|
||||
<field name="line_ids" colspan="4" nolabel="1">
|
||||
<tree string="Transaction" colors="red:duplicate;blue:reconcile_id">
|
||||
<field name="date"/>
|
||||
<field name="amount"/>
|
||||
<field name="ref"/>
|
||||
<field name="partner_bank_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="account_id"/>
|
||||
<field name="reconcile_id"/>
|
||||
<field name="duplicate"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page attrs="{'invisible': [('state', '=', 'init')]}" string="Log">
|
||||
<field name="log" colspan="4" nolabel="1" width="500"/>
|
||||
</page>
|
||||
<page attrs="{'invisible': [('state', '!=', 'ready')]}" string="Statements">
|
||||
<field name="statement_ids" colspan="4" nolabel="1"
|
||||
attrs="{'invisible': [('state', '!=', 'ready')]}" />
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<div attrs="{'invisible': [('state', '!=', 'init')]}">
|
||||
<button string="Import" class="oe_highlight"
|
||||
states="init" name="import_statements_file"
|
||||
type="object"/>
|
||||
or
|
||||
<button class="oe_link" special="cancel"
|
||||
states="init" string="Cancel"/>
|
||||
</div>
|
||||
|
||||
<button icon="gtk-close"
|
||||
special="cancel"
|
||||
string="Close"
|
||||
states="ready,error"/>
|
||||
<button icon="gtk-close"
|
||||
name="cancel_statement_lines"
|
||||
type="object"
|
||||
string="Cancel"
|
||||
states="review"/>
|
||||
<button icon="gtk-ok"
|
||||
name="create_statement_lines"
|
||||
type="object"
|
||||
string="Confirm"
|
||||
states="review"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="wizard_account_banking_import_file" model="ir.actions.act_window">
|
||||
<field name="name">Import Bank Statements File</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.banking.bank.import</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_banking_import"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,453 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
|
||||
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
|
||||
# (C) 2011 Smile (<http://smile.fr>).
|
||||
# All Rights Reserved
|
||||
#
|
||||
# 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 import orm, fields
|
||||
from openerp.tools.translate import _
|
||||
|
||||
"""
|
||||
|
||||
The banking transaction wizard is linked to a button in the statement line
|
||||
tree view. It allows the user to undo the duplicate flag, select between
|
||||
multiple matches or select a manual match.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class banking_transaction_wizard(orm.TransientModel):
|
||||
_name = 'banking.transaction.wizard'
|
||||
_description = 'Match transaction'
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
"""
|
||||
Make sure that the statement line has an import transaction
|
||||
"""
|
||||
res = super(banking_transaction_wizard, self).create(
|
||||
cr, uid, vals, context=context)
|
||||
if res and vals.get('statement_line_id'):
|
||||
line_pool = self.pool.get('account.bank.statement.line')
|
||||
line_pool.create_instant_transaction(
|
||||
cr, uid, vals['statement_line_id'], context=context)
|
||||
return res
|
||||
|
||||
def create_act_window(self, cr, uid, ids, nodestroy=True, context=None):
|
||||
"""
|
||||
Return a popup window for this model
|
||||
"""
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
return {
|
||||
'name': self._description,
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': self._name,
|
||||
'domain': [],
|
||||
'context': context,
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'res_id': ids[0],
|
||||
'nodestroy': nodestroy,
|
||||
}
|
||||
|
||||
def trigger_match(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Call the automatic matching routine for one or
|
||||
more bank transactions
|
||||
"""
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
import_transaction_obj = self.pool.get('banking.import.transaction')
|
||||
trans_id = self.read(
|
||||
cr, uid, ids[0], ['import_transaction_id'],
|
||||
context=context)['import_transaction_id'][0] # many2one tuple
|
||||
import_transaction_obj.match(cr, uid, [trans_id], context=context)
|
||||
return self.create_act_window(cr, uid, ids, context=None)
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
"""
|
||||
Implement a trigger to retrieve the corresponding move line
|
||||
when the invoice_id changes
|
||||
"""
|
||||
statement_line_obj = self.pool.get('account.bank.statement.line')
|
||||
transaction_obj = self.pool.get('banking.import.transaction')
|
||||
|
||||
if not vals or not ids:
|
||||
return True
|
||||
|
||||
wiz = self.browse(cr, uid, ids[0], context=context)
|
||||
|
||||
# The following fields get never written
|
||||
# they are just triggers for manual matching
|
||||
# which populates regular fields on the transaction
|
||||
manual_invoice_ids = vals.pop('manual_invoice_ids', [])
|
||||
manual_move_line_ids = vals.pop('manual_move_line_ids', [])
|
||||
|
||||
res = super(banking_transaction_wizard, self).write(
|
||||
cr, uid, ids, vals, context=context)
|
||||
wiz.refresh()
|
||||
|
||||
# Process the logic of the written values
|
||||
|
||||
# An invoice is selected from multiple candidates
|
||||
if vals and 'invoice_id' in vals:
|
||||
if (wiz.import_transaction_id.match_type == 'invoice' and
|
||||
wiz.import_transaction_id.invoice_id):
|
||||
found = False
|
||||
# the current value might apply
|
||||
if (wiz.move_line_id and wiz.move_line_id.invoice and
|
||||
wiz.move_line_id.invoice == wiz.invoice_id):
|
||||
found = True
|
||||
else:
|
||||
# Otherwise, retrieve the move line for this invoice
|
||||
# Given the arity of the relation, there is are always
|
||||
# multiple possibilities but the move lines here are
|
||||
# prefiltered for having account_id.type payable/receivable
|
||||
# and the regular invoice workflow should only come up with
|
||||
# one of those only.
|
||||
for move_line in wiz.import_transaction_id.move_line_ids:
|
||||
if (move_line.invoice ==
|
||||
wiz.import_transaction_id.invoice_id):
|
||||
transaction_obj.write(
|
||||
cr, uid, wiz.import_transaction_id.id,
|
||||
{'move_line_id': move_line.id, },
|
||||
context=context
|
||||
)
|
||||
statement_line_obj.write(
|
||||
cr, uid,
|
||||
wiz.import_transaction_id.statement_line_id.id,
|
||||
{
|
||||
'partner_id': (
|
||||
move_line.partner_id.id or False),
|
||||
'account_id': move_line.account_id.id,
|
||||
}, context=context)
|
||||
found = True
|
||||
break
|
||||
# Cannot match the invoice
|
||||
if not found:
|
||||
orm.except_orm(
|
||||
_("No entry found for the selected invoice"),
|
||||
_("No entry found for the selected invoice. " +
|
||||
"Try manual reconciliation."))
|
||||
|
||||
if manual_move_line_ids or manual_invoice_ids:
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
invoice_obj = self.pool.get('account.invoice')
|
||||
statement_line_obj = self.pool.get('account.bank.statement.line')
|
||||
# Rewrite *2many directive notation
|
||||
if manual_invoice_ids:
|
||||
manual_invoice_ids = (
|
||||
[i[1] for i in manual_invoice_ids if i[0] == 4] +
|
||||
[j for i in manual_invoice_ids if i[0] == 6 for j in i[2]])
|
||||
if manual_move_line_ids:
|
||||
manual_move_line_ids = (
|
||||
[i[1] for i in manual_move_line_ids if i[0] == 4] +
|
||||
[j for i in manual_move_line_ids
|
||||
if i[0] == 6 for j in i[2]])
|
||||
for wiz in self.browse(cr, uid, ids, context=context):
|
||||
# write can be called multiple times for the same values
|
||||
# that doesn't hurt above, but it does here
|
||||
if wiz.match_type and (
|
||||
len(manual_move_line_ids) > 1 or
|
||||
len(manual_invoice_ids) > 1):
|
||||
continue
|
||||
|
||||
todo = []
|
||||
|
||||
for invoice in invoice_obj.browse(
|
||||
cr, uid, manual_invoice_ids, context=context):
|
||||
found_move_line = False
|
||||
if invoice.move_id:
|
||||
for line in invoice.move_id.line_id:
|
||||
if line.account_id.type in ('receivable',
|
||||
'payable'):
|
||||
todo.append((invoice.id, line.id))
|
||||
found_move_line = True
|
||||
break
|
||||
if not found_move_line:
|
||||
raise orm.except_orm(
|
||||
_("Cannot select for reconcilion"),
|
||||
_("No entry found for the selected invoice. "))
|
||||
for move_line_id in manual_move_line_ids:
|
||||
todo_entry = [False, move_line_id]
|
||||
move_line = move_line_obj.read(
|
||||
cr,
|
||||
uid,
|
||||
move_line_id,
|
||||
['invoice'],
|
||||
context=context
|
||||
)
|
||||
if move_line['invoice']:
|
||||
todo_entry[0] = move_line['invoice'][0]
|
||||
todo.append(todo_entry)
|
||||
|
||||
while todo:
|
||||
todo_entry = todo.pop()
|
||||
move_line = move_line_obj.browse(
|
||||
cr, uid, todo_entry[1], context)
|
||||
transaction_id = wiz.import_transaction_id.id
|
||||
statement_line_id = wiz.statement_line_id.id
|
||||
|
||||
if len(todo) > 0:
|
||||
statement_line_id = wiz.statement_line_id.split_off(
|
||||
move_line.debit or -move_line.credit)[0]
|
||||
transaction_id = statement_line_obj.browse(
|
||||
cr,
|
||||
uid,
|
||||
statement_line_id,
|
||||
context=context
|
||||
).import_transaction_id.id
|
||||
|
||||
vals = {
|
||||
'move_line_id': todo_entry[1],
|
||||
'move_line_ids': [(6, 0, [todo_entry[1]])],
|
||||
'invoice_id': todo_entry[0],
|
||||
'invoice_ids': [
|
||||
(6, 0, [todo_entry[0]] if todo_entry[0] else [])
|
||||
],
|
||||
'match_type': 'manual',
|
||||
}
|
||||
|
||||
transaction_obj.clear_and_write(
|
||||
cr, uid, transaction_id, vals, context=context)
|
||||
|
||||
st_line_vals = {
|
||||
'account_id': move_line_obj.read(
|
||||
cr, uid, todo_entry[1],
|
||||
['account_id'], context=context)['account_id'][0],
|
||||
}
|
||||
|
||||
if todo_entry[0]:
|
||||
st_line_vals['partner_id'] = invoice_obj.browse(
|
||||
cr, uid, todo_entry[0], context=context
|
||||
).partner_id.commercial_partner_id.id
|
||||
|
||||
statement_line_obj.write(
|
||||
cr, uid, statement_line_id,
|
||||
st_line_vals, context=context)
|
||||
return res
|
||||
|
||||
def trigger_write(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Just a button that triggers a write.
|
||||
"""
|
||||
return self.create_act_window(cr, uid, ids, context=None)
|
||||
|
||||
def disable_match(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Clear manual and automatic match information
|
||||
"""
|
||||
settings_pool = self.pool.get('account.banking.account.settings')
|
||||
statement_pool = self.pool.get('account.bank.statement.line')
|
||||
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
|
||||
for wiz in self.browse(cr, uid, ids, context=context):
|
||||
# Get the bank account setting record, to reset the account
|
||||
account_id = False
|
||||
journal_id = wiz.statement_line_id.statement_id.journal_id.id
|
||||
setting_ids = settings_pool.find(
|
||||
cr, uid, journal_id, context=context
|
||||
)
|
||||
|
||||
# Restore partner id from the bank account or else reset
|
||||
partner_id = False
|
||||
if (wiz.statement_line_id.partner_bank_id and
|
||||
wiz.statement_line_id.partner_bank_id.partner_id):
|
||||
partner_id = (
|
||||
wiz.statement_line_id.partner_bank_id.partner_id.id
|
||||
)
|
||||
wiz.write({'partner_id': partner_id})
|
||||
|
||||
bank_partner = False
|
||||
if partner_id:
|
||||
bank_partner = wiz.statement_line_id.partner_bank_id.partner_id
|
||||
if wiz.amount < 0:
|
||||
if bank_partner:
|
||||
account_id = bank_partner.\
|
||||
def_journal_account_bank_decr()[bank_partner.id]
|
||||
elif setting_ids:
|
||||
account_id = settings_pool.browse(
|
||||
cr, uid, setting_ids[0],
|
||||
context=context).default_credit_account_id.id
|
||||
else:
|
||||
if bank_partner:
|
||||
account_id = bank_partner.\
|
||||
def_journal_account_bank_incr()[bank_partner.id]
|
||||
elif setting_ids:
|
||||
account_id = settings_pool.browse(
|
||||
cr, uid, setting_ids[0],
|
||||
context=context).default_debit_account_id.id
|
||||
|
||||
if account_id:
|
||||
wiz.statement_line_id.write({'account_id': account_id})
|
||||
|
||||
if wiz.statement_line_id:
|
||||
# delete splits causing an unsplit if this is a split
|
||||
# transaction
|
||||
statement_pool.unlink(
|
||||
cr,
|
||||
uid,
|
||||
statement_pool.search(
|
||||
cr, uid,
|
||||
[('parent_id', '=', wiz.statement_line_id.id)],
|
||||
context=context
|
||||
),
|
||||
context=context
|
||||
)
|
||||
|
||||
if wiz.import_transaction_id:
|
||||
wiz.import_transaction_id.clear_and_write()
|
||||
|
||||
return self.create_act_window(cr, uid, ids, context=None)
|
||||
|
||||
def reverse_duplicate(self, cr, uid, ids, context=None):
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
transaction_obj = self.pool.get('banking.import.transaction')
|
||||
for wiz in self.read(
|
||||
cr, uid, ids, ['duplicate', 'import_transaction_id'],
|
||||
context=context):
|
||||
transaction_obj.write(
|
||||
cr, uid, wiz['import_transaction_id'][0],
|
||||
{'duplicate': not wiz['duplicate']}, context=context)
|
||||
return self.create_act_window(cr, uid, ids, context=None)
|
||||
|
||||
def button_done(self, cr, uid, ids, context=None):
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=64),
|
||||
'statement_line_id': fields.many2one(
|
||||
'account.bank.statement.line', 'Statement line',
|
||||
),
|
||||
'amount': fields.related(
|
||||
'statement_line_id', 'amount', type='float',
|
||||
string="Amount", readonly=True),
|
||||
'date': fields.related(
|
||||
'statement_line_id', 'date', type='date',
|
||||
string="Date", readonly=True),
|
||||
'ref': fields.related(
|
||||
'statement_line_id', 'ref', type='char', size=32,
|
||||
string="Reference", readonly=True),
|
||||
'message': fields.related(
|
||||
'statement_line_id', 'import_transaction_id', 'message',
|
||||
type='char', size=1024,
|
||||
string="Message", readonly=True),
|
||||
'partner_id': fields.related(
|
||||
'statement_line_id', 'partner_id',
|
||||
type='many2one', relation='res.partner',
|
||||
string="Partner", readonly=True),
|
||||
'statement_line_parent_id': fields.related(
|
||||
'statement_line_id', 'parent_id', type='many2one',
|
||||
relation='account.bank.statement.line', readonly=True),
|
||||
'import_transaction_id': fields.related(
|
||||
'statement_line_id', 'import_transaction_id',
|
||||
string="Import transaction",
|
||||
type='many2one', relation='banking.import.transaction'),
|
||||
'residual': fields.related(
|
||||
'import_transaction_id', 'residual', type='float',
|
||||
string='Residual', readonly=True),
|
||||
'writeoff_account_id': fields.related(
|
||||
'import_transaction_id', 'writeoff_account_id',
|
||||
type='many2one', relation='account.account',
|
||||
string='Write-off account'),
|
||||
'invoice_ids': fields.related(
|
||||
'import_transaction_id', 'invoice_ids', string="Matching invoices",
|
||||
type='many2many', relation='account.invoice'),
|
||||
'invoice_id': fields.related(
|
||||
'import_transaction_id',
|
||||
'invoice_id',
|
||||
string="Invoice to reconcile",
|
||||
type='many2one',
|
||||
relation='account.invoice',
|
||||
),
|
||||
'move_line_ids': fields.related(
|
||||
'import_transaction_id', 'move_line_ids', string="Entry lines",
|
||||
type='many2many', relation='account.move.line'),
|
||||
'move_line_id': fields.related(
|
||||
'import_transaction_id', 'move_line_id', string="Entry line",
|
||||
type='many2one', relation='account.move.line'),
|
||||
'duplicate': fields.related(
|
||||
'import_transaction_id',
|
||||
'duplicate',
|
||||
string='Flagged as duplicate',
|
||||
type='boolean',
|
||||
),
|
||||
'match_multi': fields.related(
|
||||
'import_transaction_id', 'match_multi',
|
||||
type="boolean", string='Multiple matches'),
|
||||
'match_type': fields.related(
|
||||
'import_transaction_id',
|
||||
'match_type',
|
||||
type='selection',
|
||||
selection=[
|
||||
('move', 'Move'),
|
||||
('invoice', 'Invoice'),
|
||||
('payment', 'Payment line'),
|
||||
('payment_order', 'Payment order'),
|
||||
('storno', 'Storno'),
|
||||
('manual', 'Manual'),
|
||||
('payment_manual', 'Payment line (manual)'),
|
||||
('payment_order_manual', 'Payment order (manual)'),
|
||||
],
|
||||
string='Match type',
|
||||
readonly=True,
|
||||
),
|
||||
'manual_invoice_ids': fields.many2many(
|
||||
'account.invoice',
|
||||
'banking_transaction_wizard_account_invoice_rel',
|
||||
'wizard_id', 'invoice_id', string='Match one or more invoices',
|
||||
domain=[('reconciled', '=', False)]),
|
||||
'manual_move_line_ids': fields.many2many(
|
||||
'account.move.line',
|
||||
'banking_transaction_wizard_account_move_line_rel',
|
||||
'wizard_id', 'move_line_id', string='Or match one or more entries',
|
||||
domain=[('account_id.reconcile', '=', True),
|
||||
('reconcile_id', '=', False)]),
|
||||
'payment_option': fields.related(
|
||||
'import_transaction_id',
|
||||
'payment_option',
|
||||
string='Payment Difference',
|
||||
type='selection',
|
||||
required=True,
|
||||
selection=[
|
||||
('without_writeoff', 'Keep Open'),
|
||||
('with_writeoff', 'Reconcile Payment Balance')
|
||||
],
|
||||
),
|
||||
'writeoff_analytic_id': fields.related(
|
||||
'import_transaction_id', 'writeoff_analytic_id',
|
||||
type='many2one', relation='account.analytic.account',
|
||||
string='Write-off analytic account'),
|
||||
'analytic_account_id': fields.related(
|
||||
'statement_line_id', 'analytic_account_id',
|
||||
type='many2one', relation='account.analytic.account',
|
||||
string="Analytic Account"),
|
||||
'move_currency_amount': fields.related(
|
||||
'import_transaction_id',
|
||||
'move_currency_amount',
|
||||
type='float',
|
||||
string='Match Currency Amount',
|
||||
readonly=True,
|
||||
),
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="transaction_wizard_first">
|
||||
<field name="name">transaction.wizard.first</field>
|
||||
<field name="model">banking.transaction.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Match transaction" version="7.0">
|
||||
<!-- fields used for form logic -->
|
||||
<field name="statement_line_parent_id" invisible="True"/>
|
||||
<field name="invoice_ids" invisible="True"/>
|
||||
<field name="move_line_ids" invisible="True"/>
|
||||
<field name="match_multi" invisible="True"/>
|
||||
<field name="duplicate" invisible="True"/>
|
||||
<group colspan="2" col="2">
|
||||
<group colspan="2" col="4">
|
||||
<separator string="Transaction data" colspan="4"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="date"/>
|
||||
<field name="amount"/>
|
||||
<field name="ref"/>
|
||||
<field name="message" colspan="4"/>
|
||||
</group>
|
||||
|
||||
<!-- (semi-) automatic matching and selection -->
|
||||
|
||||
<group colspan="2" col="4">
|
||||
|
||||
<separator string="Current match" colspan="4"/>
|
||||
<field name="match_type"/>
|
||||
<newline />
|
||||
<field name="move_currency_amount" />
|
||||
<newline />
|
||||
<field name="residual"/>
|
||||
<group attrs="{'invisible': [('match_multi', '=', False)]}" colspan="4" col="2">
|
||||
<separator string="Multiple matches" colspan="2"/>
|
||||
<label colspan="2" string="Multiple matches were found for this bank transfer. You must pick one of the matches or select a match manually below." />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<newline/>
|
||||
<!-- show if we have an invoice type match (but the user may need to select from multiple options)
|
||||
or whenever there is an invoice_id (e.g. in case of a manual match)
|
||||
-->
|
||||
<field name='invoice_id'
|
||||
attrs="{'readonly': [('match_multi', '=', False)], 'invisible': [('match_type', '!=', 'invoice'),('invoice_id', '=', False)]}" colspan="2"
|
||||
domain="[('id', 'in', invoice_ids[0][2])]"
|
||||
/>
|
||||
<!-- show if we have a move type match or a manual match without an invoice_id
|
||||
-->
|
||||
<field name='move_line_id'
|
||||
attrs="{'readonly': [('match_multi', '=', False)], 'invisible': [('match_type', '!=', 'move'),('invoice_id', '=', False)]}" colspan="2"
|
||||
domain="[('id', 'in', move_line_ids[0][2])]"
|
||||
/>
|
||||
<newline/>
|
||||
<field colspan="2" name='analytic_account_id' />
|
||||
<button colspan="1"
|
||||
name="trigger_write"
|
||||
type="object"
|
||||
attrs="{'invisible': [('match_multi', '=', False)]}"
|
||||
string="Select"/>
|
||||
<newline/>
|
||||
|
||||
<!-- residual and write off -->
|
||||
|
||||
</group>
|
||||
<notebook>
|
||||
<!-- Duplicate flagging -->
|
||||
<page string="Duplicate" attrs="{'invisible': [('duplicate', '=', False)]}">
|
||||
<group colspan="2" col="2">
|
||||
<label colspan="2" string="This bank transfer was marked as a duplicate. You can either confirm that this is not the case, or remove the bank transfer from the system."/>
|
||||
<newline/>
|
||||
<button colspan="1"
|
||||
name="reverse_duplicate"
|
||||
type="object"
|
||||
string="Remove duplicate flag"/>
|
||||
</group>
|
||||
</page>
|
||||
<!-- Redo automatic match -->
|
||||
<page string="Match again">
|
||||
<label string="You can let the system try to match this bank statement line again after you have made any changes in the database (for instance, add an invoice or a bank account)." colspan="2"/>
|
||||
<newline/>
|
||||
<button colspan="1"
|
||||
name="trigger_match"
|
||||
type="object"
|
||||
string="Match again"/>
|
||||
</page>
|
||||
<!-- Manual selection -->
|
||||
<page string="Manual match">
|
||||
<separator string="Match one or more invoices" colspan="4"/>
|
||||
<field name="manual_invoice_ids" colspan="4"
|
||||
context="{'search_default_partner_id': partner_id}"
|
||||
/>
|
||||
<separator string="Or match one or more entries" colspan="4"/>
|
||||
<field name="manual_move_line_ids" colspan="4"
|
||||
context="{'search_default_partner_id': partner_id}"
|
||||
/>
|
||||
<button name="trigger_write"
|
||||
type="object"
|
||||
string="Match" />
|
||||
</page>
|
||||
<page string="Write-Off" attrs="{'invisible': [('match_type', '=', False)]}">
|
||||
<group colspan="2" col="2">
|
||||
<label string="Choose what you want to do with the eventual difference between the paid amount and the sum of allocated amounts. You can either choose to keep open this difference on the partner's account, or reconcile it with the payment." colspan="2"/>
|
||||
<field name="payment_option" />
|
||||
<field name="writeoff_account_id" attrs="{'required':[('payment_option','=','with_writeoff')],'invisible':[('payment_option','=','without_writeoff')]}" />
|
||||
<field name="writeoff_analytic_id"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
attrs="{'invisible':[('payment_option','=','without_writeoff')]}" />
|
||||
<button colspan="1"
|
||||
name="trigger_write"
|
||||
type="object"
|
||||
string="Set write-off account"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Disable reconciliation" attrs="{'invisible': [('match_type', '==', False)]}">
|
||||
<group colspan="2" col="2">
|
||||
<label string="You can disable the reconciliation of this bank transfer" colspan="2"/>
|
||||
<newline/>
|
||||
<button colspan="1"
|
||||
name="disable_match"
|
||||
type="object"
|
||||
string="Disable reconciliation"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button icon="gtk-ok" string="Close" special="cancel"/>
|
||||
</footer>
|
||||
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,338 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
|
||||
# All Rights Reserved
|
||||
#
|
||||
# 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.tools.translate import _
|
||||
from openerp.addons.account_banking import sepa
|
||||
from openerp.addons.account_banking.struct import struct
|
||||
|
||||
__all__ = [
|
||||
'get_period',
|
||||
'get_bank_accounts',
|
||||
'get_partner',
|
||||
'get_country_id',
|
||||
'get_company_bank_account',
|
||||
'create_bank_account',
|
||||
]
|
||||
|
||||
|
||||
def get_period(pool, cr, uid, date, company, log=None):
|
||||
'''
|
||||
Wrapper over account_period.find() to log exceptions of
|
||||
missing periods instead of raising.
|
||||
'''
|
||||
context = {'account_period_prefer_normal': True}
|
||||
if company:
|
||||
context['company_id'] = company.id
|
||||
try:
|
||||
period_ids = pool.get('account.period').find(
|
||||
cr, uid, dt=date, context=context)
|
||||
except Exception as e:
|
||||
if log is None:
|
||||
raise
|
||||
else:
|
||||
log.append(e)
|
||||
return False
|
||||
return period_ids[0]
|
||||
|
||||
|
||||
def get_bank_accounts(pool, cr, uid, account_number, log, fail=False):
|
||||
'''
|
||||
Get the bank account with account number account_number
|
||||
'''
|
||||
# No need to search for nothing
|
||||
if not account_number:
|
||||
return []
|
||||
|
||||
partner_bank_obj = pool.get('res.partner.bank')
|
||||
bank_account_ids = partner_bank_obj.search(cr, uid, [
|
||||
('acc_number', '=', account_number)
|
||||
])
|
||||
if not bank_account_ids:
|
||||
if not fail:
|
||||
log.append(
|
||||
_('Bank account %(account_no)s was not found in the database')
|
||||
% dict(account_no=account_number)
|
||||
)
|
||||
return []
|
||||
return partner_bank_obj.browse(cr, uid, bank_account_ids)
|
||||
|
||||
|
||||
def _has_attr(obj, attr):
|
||||
# Needed for dangling addresses and a weird exception scheme in
|
||||
# OpenERP's orm.
|
||||
try:
|
||||
return bool(getattr(obj, attr))
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
|
||||
def get_partner(pool, cr, uid, name, address, postal_code, city,
|
||||
country_id, log, context=None):
|
||||
'''
|
||||
Get the partner belonging to the account holders name <name>
|
||||
|
||||
If multiple partners are found with the same name, select the first and
|
||||
add a warning to the import log.
|
||||
|
||||
TODO: revive the search by lines from the address argument
|
||||
'''
|
||||
partner_obj = pool.get('res.partner')
|
||||
partner_ids = partner_obj.search(
|
||||
cr, uid, [
|
||||
'|', ('is_company', '=', True), ('parent_id', '=', False),
|
||||
('name', 'ilike', name),
|
||||
], context=context)
|
||||
if not partner_ids:
|
||||
# Try brute search on address and then match reverse
|
||||
criteria = []
|
||||
if country_id:
|
||||
criteria.append(('country_id', '=', country_id))
|
||||
if city:
|
||||
criteria.append(('city', 'ilike', city))
|
||||
if postal_code:
|
||||
criteria.append(('zip', 'ilike', postal_code))
|
||||
partner_search_ids = partner_obj.search(
|
||||
cr, uid, criteria, context=context)
|
||||
if (not partner_search_ids and country_id):
|
||||
# Try again with country_id = False
|
||||
criteria[0] = ('country_id', '=', False)
|
||||
partner_search_ids = partner_obj.search(
|
||||
cr, uid, criteria, context=context)
|
||||
key = name.lower()
|
||||
partners = []
|
||||
for partner in partner_obj.read(
|
||||
cr, uid, partner_search_ids, ['name', 'commercial_partner_id'],
|
||||
context=context):
|
||||
if (len(partner['name']) > 3 and partner['name'].lower() in key):
|
||||
partners.append(partner)
|
||||
partners.sort(key=lambda x: len(x['name']), reverse=True)
|
||||
partner_ids = [x['commercial_partner_id'][0] for x in partners]
|
||||
if len(partner_ids) > 1:
|
||||
log.append(
|
||||
_('More than one possible match found for partner with '
|
||||
'name %(name)s') % {'name': name})
|
||||
return partner_ids and partner_ids[0] or False
|
||||
|
||||
|
||||
def get_company_bank_account(pool, cr, uid, account_number, currency,
|
||||
company, log):
|
||||
'''
|
||||
Get the matching bank account for this company. Currency is the ISO code
|
||||
for the requested currency.
|
||||
'''
|
||||
results = struct()
|
||||
bank_accounts = get_bank_accounts(pool, cr, uid, account_number, log,
|
||||
fail=True)
|
||||
if not bank_accounts:
|
||||
return False
|
||||
elif len(bank_accounts) != 1:
|
||||
log.append(
|
||||
_('More than one bank account was found with the same number '
|
||||
'%(account_no)s') % dict(account_no=account_number)
|
||||
)
|
||||
return False
|
||||
if bank_accounts[0].partner_id.id != company.partner_id.id:
|
||||
log.append(
|
||||
_('Account %(account_no)s is not owned by %(partner)s')
|
||||
% dict(account_no=account_number,
|
||||
partner=company.partner_id.name,
|
||||
))
|
||||
return False
|
||||
results.account = bank_accounts[0]
|
||||
bank_settings_obj = pool.get('account.banking.account.settings')
|
||||
criteria = [('partner_bank_id', '=', bank_accounts[0].id)]
|
||||
|
||||
# Find matching journal for currency
|
||||
journal_obj = pool.get('account.journal')
|
||||
journal_ids = journal_obj.search(cr, uid, [
|
||||
('type', '=', 'bank'),
|
||||
('currency.name', '=', currency or company.currency_id.name)
|
||||
])
|
||||
if currency == company.currency_id.name:
|
||||
journal_ids_no_curr = journal_obj.search(cr, uid, [
|
||||
('type', '=', 'bank'), ('currency', '=', False)
|
||||
])
|
||||
journal_ids.extend(journal_ids_no_curr)
|
||||
if journal_ids:
|
||||
criteria.append(('journal_id', 'in', journal_ids))
|
||||
|
||||
# Find bank account settings
|
||||
bank_settings_ids = bank_settings_obj.search(cr, uid, criteria)
|
||||
if bank_settings_ids:
|
||||
settings = bank_settings_obj.browse(cr, uid, bank_settings_ids)[0]
|
||||
results.company_id = company
|
||||
results.journal_id = settings.journal_id
|
||||
|
||||
# Take currency from settings or from company
|
||||
if settings.journal_id.currency.id:
|
||||
results.currency_id = settings.journal_id.currency
|
||||
else:
|
||||
results.currency_id = company.currency_id
|
||||
# Rest just copy/paste from settings. Why am I doing this?
|
||||
results.default_debit_account_id = settings.default_debit_account_id
|
||||
results.default_credit_account_id = settings.default_credit_account_id
|
||||
results.costs_account_id = settings.costs_account_id
|
||||
results.invoice_journal_id = settings.invoice_journal_id
|
||||
results.bank_partner_id = settings.bank_partner_id
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def get_or_create_bank(pool, cr, uid, bic, online=False, code=None,
|
||||
name=None, context=None):
|
||||
'''
|
||||
Find or create the bank with the provided BIC code.
|
||||
When online, the SWIFT database will be consulted in order to
|
||||
provide for missing information.
|
||||
'''
|
||||
# UPDATE: Free SWIFT databases are since 2/22/2010 hidden behind an
|
||||
# image challenge/response interface.
|
||||
|
||||
bank_obj = pool.get('res.bank')
|
||||
|
||||
# Self generated key?
|
||||
if len(bic) < 8:
|
||||
# search key
|
||||
bank_ids = bank_obj.search(
|
||||
cr, uid, [
|
||||
('bic', '=', bic[:6])
|
||||
])
|
||||
if not bank_ids:
|
||||
bank_ids = bank_obj.search(
|
||||
cr, uid, [
|
||||
('bic', 'ilike', bic + '%')
|
||||
])
|
||||
else:
|
||||
bank_ids = bank_obj.search(
|
||||
cr, uid, [
|
||||
('bic', '=', bic)
|
||||
])
|
||||
|
||||
if bank_ids and len(bank_ids) == 1:
|
||||
banks = bank_obj.browse(cr, uid, bank_ids)
|
||||
return banks[0].id, banks[0].country.id
|
||||
|
||||
country_obj = pool.get('res.country')
|
||||
country_ids = country_obj.search(
|
||||
cr, uid, [('code', '=', bic[4:6])]
|
||||
)
|
||||
country_id = country_ids and country_ids[0] or False
|
||||
bank_id = False
|
||||
|
||||
if online:
|
||||
info, address = bank_obj.online_bank_info(
|
||||
cr, uid, bic, context=context
|
||||
)
|
||||
if info:
|
||||
bank_id = bank_obj.create(cr, uid, dict(
|
||||
code=info.code,
|
||||
name=info.name,
|
||||
street=address.street,
|
||||
street2=address.street2,
|
||||
zip=address.zip,
|
||||
city=address.city,
|
||||
country=country_id,
|
||||
bic=info.bic[:8],
|
||||
))
|
||||
else:
|
||||
info = struct(name=name, code=code)
|
||||
|
||||
if not online or not bank_id:
|
||||
bank_id = bank_obj.create(cr, uid, dict(
|
||||
code=info.code or 'UNKNOW', # FIXME: Typo?
|
||||
name=info.name or _('Unknown Bank'),
|
||||
country=country_id,
|
||||
bic=bic,
|
||||
))
|
||||
return bank_id, country_id
|
||||
|
||||
|
||||
def get_country_id(pool, cr, uid, transaction, context=None):
|
||||
"""
|
||||
Derive a country id from the info on the transaction.
|
||||
|
||||
:param transaction: browse record of a transaction
|
||||
:returns: res.country id or False
|
||||
"""
|
||||
|
||||
country_code = False
|
||||
iban = sepa.IBAN(transaction.remote_account)
|
||||
if iban.valid:
|
||||
country_code = iban.countrycode
|
||||
elif transaction.remote_owner_country_code:
|
||||
country_code = transaction.remote_owner_country_code
|
||||
# fallback on the import parsers country code
|
||||
elif transaction.bank_country_code:
|
||||
country_code = transaction.bank_country_code
|
||||
if country_code:
|
||||
country_ids = pool.get('res.country').search(
|
||||
cr, uid, [('code', '=', country_code.upper())],
|
||||
context=context)
|
||||
country_id = country_ids and country_ids[0] or False
|
||||
if not country_id:
|
||||
company = transaction.statement_line_id.company_id
|
||||
if company.partner_id.country:
|
||||
country_id = company.partner_id.country.id
|
||||
return country_id
|
||||
|
||||
|
||||
def create_bank_account(pool, cr, uid, partner_id,
|
||||
account_number, holder_name, address, city,
|
||||
country_id, bic=False,
|
||||
context=None):
|
||||
'''
|
||||
Create a matching bank account with this holder for this partner.
|
||||
'''
|
||||
values = struct(
|
||||
partner_id=partner_id,
|
||||
owner_name=holder_name,
|
||||
country_id=country_id,
|
||||
)
|
||||
|
||||
# Are we dealing with IBAN?
|
||||
iban = sepa.IBAN(account_number)
|
||||
if iban.valid:
|
||||
# Take as much info as possible from IBAN
|
||||
values.state = 'iban'
|
||||
values.acc_number = str(iban)
|
||||
else:
|
||||
# No, try to convert to IBAN
|
||||
values.state = 'bank'
|
||||
values.acc_number = account_number
|
||||
|
||||
if country_id:
|
||||
country_code = pool.get('res.country').read(
|
||||
cr, uid, country_id, ['code'], context=context)['code']
|
||||
if country_code in sepa.IBAN.countries:
|
||||
account_info = pool['res.partner.bank'].online_account_info(
|
||||
cr, uid, country_code, values.acc_number, context=context)
|
||||
if account_info:
|
||||
values.acc_number = iban = account_info.iban
|
||||
values.state = 'iban'
|
||||
bic = account_info.bic
|
||||
|
||||
if bic:
|
||||
values.bank = get_or_create_bank(pool, cr, uid, bic)[0]
|
||||
values.bank_bic = bic
|
||||
|
||||
# Create bank account and return
|
||||
return pool.get('res.partner.bank').create(
|
||||
cr, uid, values, context=context)
|
||||
@@ -1,208 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (C) 2013 Therp BV (<http://therp.nl>).
|
||||
# All Rights Reserved
|
||||
#
|
||||
# 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.osv import orm, fields
|
||||
from openerp.tools.translate import _
|
||||
from openerp.addons.account_banking.wizard import banktools
|
||||
import ast
|
||||
|
||||
|
||||
class link_partner(orm.TransientModel):
|
||||
_name = 'banking.link_partner'
|
||||
_description = 'Link partner'
|
||||
|
||||
_columns = {
|
||||
'name': fields.char(
|
||||
'Create partner with name', size=128, required=True),
|
||||
'supplier': fields.boolean('Supplier'),
|
||||
'customer': fields.boolean('Customer'),
|
||||
'partner_id': fields.many2one(
|
||||
'res.partner', 'or link existing partner',
|
||||
domain=['|', ('is_company', '=', True),
|
||||
('parent_id', '=', False)],
|
||||
),
|
||||
'statement_line_id': fields.many2one(
|
||||
'account.bank.statement.line',
|
||||
'Statement line', required=True),
|
||||
'amount': fields.related(
|
||||
'statement_line_id', 'amount', type='float',
|
||||
string="Amount", readonly=True),
|
||||
'ref': fields.related(
|
||||
'statement_line_id', 'ref', type='char', size=32,
|
||||
string="Reference", readonly=True),
|
||||
'message': fields.related(
|
||||
'statement_line_id', 'import_transaction_id', 'message',
|
||||
type='char', size=1024,
|
||||
string="Message", readonly=True),
|
||||
'remote_account': fields.char(
|
||||
'Account number', size=24, readonly=True),
|
||||
# Partner values
|
||||
'street': fields.char('Street', size=128),
|
||||
'street2': fields.char('Street2', size=128),
|
||||
'zip': fields.char('Zip', change_default=True, size=24),
|
||||
'city': fields.char('City', size=128),
|
||||
'state_id': fields.many2one("res.country.state", 'State'),
|
||||
'country_id': fields.many2one('res.country', 'Country'),
|
||||
'email': fields.char('Email', size=240),
|
||||
'phone': fields.char('Phone', size=64),
|
||||
'fax': fields.char('Fax', size=64),
|
||||
'mobile': fields.char('Mobile', size=64),
|
||||
'is_company': fields.boolean('Is a Company'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'is_company': True,
|
||||
}
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
"""
|
||||
Get default values from the transaction data
|
||||
on the statement line
|
||||
"""
|
||||
if vals and vals.get('statement_line_id'):
|
||||
statement_line_obj = self.pool.get('account.bank.statement.line')
|
||||
statement_line = statement_line_obj.browse(
|
||||
cr, uid, vals['statement_line_id'], context=context)
|
||||
transaction = statement_line.import_transaction_id
|
||||
|
||||
if statement_line.partner_bank_id:
|
||||
raise orm.except_orm(
|
||||
_('Error'),
|
||||
_('Statement line is already linked to a bank account '))
|
||||
|
||||
if not(transaction
|
||||
and transaction.remote_account):
|
||||
raise orm.except_orm(
|
||||
_('Error'),
|
||||
_('No transaction data on statement line'))
|
||||
|
||||
if 'supplier' not in vals and statement_line.amount < 0:
|
||||
vals['supplier'] = True
|
||||
if 'customer' not in vals and statement_line.amount > 0:
|
||||
vals['customer'] = True
|
||||
|
||||
address_list = []
|
||||
try:
|
||||
address_list = ast.literal_eval(
|
||||
transaction.remote_owner_address or [])
|
||||
except ValueError:
|
||||
pass
|
||||
if address_list and not vals.get('street'):
|
||||
vals['street'] = address_list.pop(0)
|
||||
if address_list and not vals.get('street2'):
|
||||
vals['street2'] = address_list.pop(0)
|
||||
if transaction.remote_owner_postalcode and not vals.get('zip'):
|
||||
vals['zip'] = transaction.remote_owner_postalcode
|
||||
if transaction.remote_owner_city and not vals.get('city'):
|
||||
vals['city'] = transaction.remote_owner_city
|
||||
if not vals.get('country_id'):
|
||||
vals['country_id'] = banktools.get_country_id(
|
||||
self.pool, cr, uid, transaction, context=context)
|
||||
if not vals.get('name'):
|
||||
vals['name'] = transaction.remote_owner
|
||||
if not vals['name']:
|
||||
vals['name'] = '/'
|
||||
if not vals.get('remote_account'):
|
||||
vals['remote_account'] = transaction.remote_account
|
||||
|
||||
return super(link_partner, self).create(
|
||||
cr, uid, vals, context=context)
|
||||
|
||||
def update_partner_values(self, cr, uid, wizard, values, context=None):
|
||||
"""
|
||||
Updates the new partner values with the values from the wizard
|
||||
|
||||
:param wizard: read record of wizard (with load='_classic_write')
|
||||
:param values: the dictionary of partner values that will be updated
|
||||
"""
|
||||
for field in ['is_company',
|
||||
'name',
|
||||
'street',
|
||||
'street2',
|
||||
'zip',
|
||||
'city',
|
||||
'country_id',
|
||||
'state_id',
|
||||
'phone',
|
||||
'fax',
|
||||
'mobile',
|
||||
'email'
|
||||
]:
|
||||
if wizard[field]:
|
||||
values[field] = wizard[field]
|
||||
return True
|
||||
|
||||
def link_partner(self, cr, uid, ids, context=None):
|
||||
statement_line_obj = self.pool.get(
|
||||
'account.bank.statement.line')
|
||||
wiz = self.browse(cr, uid, ids[0], context=context)
|
||||
|
||||
if wiz.partner_id:
|
||||
partner_id = wiz.partner_id.id
|
||||
else:
|
||||
wiz_read = self.read(
|
||||
cr, uid, ids[0], context=context, load='_classic_write')
|
||||
partner_vals = {
|
||||
'type': 'default',
|
||||
}
|
||||
self.update_partner_values(
|
||||
cr, uid, wiz_read, partner_vals, context=context)
|
||||
partner_id = self.pool.get('res.partner').create(
|
||||
cr, uid, partner_vals, context=context)
|
||||
|
||||
partner_bank_id = banktools.create_bank_account(
|
||||
self.pool, cr, uid, partner_id,
|
||||
wiz.remote_account, wiz.name,
|
||||
wiz.street, wiz.city,
|
||||
wiz.country_id and wiz.country_id.id or False,
|
||||
bic=wiz.statement_line_id.import_transaction_id.remote_bank_bic,
|
||||
context=context)
|
||||
|
||||
statement_line_ids = statement_line_obj.search(
|
||||
cr, uid,
|
||||
[('import_transaction_id.remote_account', '=', wiz.remote_account),
|
||||
('partner_bank_id', '=', False),
|
||||
('state', '=', 'draft')], context=context)
|
||||
statement_line_obj.write(
|
||||
cr, uid, statement_line_ids,
|
||||
{'partner_bank_id': partner_bank_id,
|
||||
'partner_id': partner_id}, context=context)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def create_act_window(self, cr, uid, ids, nodestroy=True, context=None):
|
||||
"""
|
||||
Return a popup window for this model
|
||||
"""
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
return {
|
||||
'name': self._description,
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': self._name,
|
||||
'domain': [],
|
||||
'context': context,
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'res_id': ids[0],
|
||||
'nodestroy': nodestroy,
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="link_partner_view">
|
||||
<field name="name">Link partner wizard view</field>
|
||||
<field name="type">form</field>
|
||||
<field name="model">banking.link_partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Link partner" version="7.0" >
|
||||
<group colspan="4" col="6" string="Transaction data">
|
||||
<field name="ref" />
|
||||
<field name="amount" />
|
||||
<field name="remote_account" />
|
||||
<field name="message" colspan="6"/>
|
||||
</group>
|
||||
<group colspan="4" col="4" string="Create or link partner">
|
||||
<field name="name"
|
||||
attrs="{'readonly': [('partner_id', '!=', False)]}" />
|
||||
<field name="partner_id"/>
|
||||
</group>
|
||||
<group colspan="2">
|
||||
<field name="is_company" />
|
||||
</group>
|
||||
<group colspan="4"
|
||||
string="Address"
|
||||
attrs="{'invisible': [('partner_id', '!=', False)]}"
|
||||
col="4">
|
||||
<group colspan="2" col="2">
|
||||
<field name="street"/>
|
||||
<field name="street2"/>
|
||||
<field name="zip"/>
|
||||
<field name="city"/>
|
||||
<field name="country_id"/>
|
||||
<field name="state_id"/>
|
||||
</group>
|
||||
<group colspan="2" col="2">
|
||||
<field name="phone"/>
|
||||
<field name="fax"/>
|
||||
<field name="mobile"/>
|
||||
<field name="email" widget="email"/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Create partner"
|
||||
name="link_partner" type="object"
|
||||
class="oe_highlight"
|
||||
attrs="{'invisible': [('partner_id', '!=', False)]}"
|
||||
/>
|
||||
<button string="Link existing partner"
|
||||
class="oe_highlight"
|
||||
name="link_partner" type="object"
|
||||
attrs="{'invisible': [('partner_id', '==', False)]}"
|
||||
/>
|
||||
or
|
||||
<button class="oe_link" string="Cancel" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
Reference in New Issue
Block a user