credativ team mailing list archive
-
credativ team
-
Mailing list archive
-
Message #04709
lp:~therp-nl/banking-addons/interactive-fix-partial-unreconcile into lp:~banking-addons-team/banking-addons/6.1-dev
Stefan Rijnhart (Therp) has proposed merging lp:~therp-nl/banking-addons/interactive-fix-partial-unreconcile into lp:~banking-addons-team/banking-addons/6.1-dev.
Requested reviews:
James Jesudason (jamesj)
For more details, see:
https://code.launchpad.net/~therp-nl/banking-addons/interactive-fix-partial-unreconcile/+merge/93975
This branch fixes an attempt to copy() a list. In addition, a logical error was fixed in the related code path of partial unreconciliation.
--
https://code.launchpad.net/~therp-nl/banking-addons/interactive-fix-partial-unreconcile/+merge/93975
Your team Banking Addons Team is subscribed to branch lp:~banking-addons-team/banking-addons/6.1-dev.
=== modified file 'account_banking/__openerp__.py'
--- account_banking/__openerp__.py 2012-02-19 21:10:20 +0000
+++ account_banking/__openerp__.py 2012-02-21 12:27:17 +0000
@@ -32,11 +32,18 @@
'name': 'Account Banking',
'version': '0.1.106',
'license': 'GPL-3',
+<<<<<<< TREE
'author': 'Banking addons community',
'website': 'https://launchpad.net/banking-addons',
'category': 'Banking addons',
'depends': ['base', 'account', 'base_iban', 'account_payment',
'account_iban_preserve_domestic'],
+=======
+ 'author': 'Banking addons community',
+ 'website': 'https://launchpad.net/banking-addons',
+ 'category': 'Banking addons',
+ 'depends': ['base', 'account', 'base_iban', 'account_payment'],
+>>>>>>> MERGE-SOURCE
'init_xml': [],
'update_xml': [
'security/ir.model.access.csv',
=== modified file 'account_banking/account_banking.py'
--- account_banking/account_banking.py 2012-02-20 19:41:49 +0000
+++ account_banking/account_banking.py 2012-02-21 12:27:17 +0000
@@ -457,10 +457,17 @@
- Pay invoices through workflow
"""
if st_line.reconcile_id:
+<<<<<<< TREE
account_move_line_obj.write(cr, uid, torec, {
(st_line.reconcile_id.line_partial_ids and
'reconcile_partial_id' or 'reconcile_id'):
st_line.reconcile_id.id }, context=context)
+=======
+ account_move_line_obj.write(cr, uid, [torec], {
+ (st_line.reconcile_id.line_partial_ids and
+ 'reconcile_partial_id' or 'reconcile_id'):
+ st_line.reconcile_id.id }, context=context)
+>>>>>>> MERGE-SOURCE
for move_line in (st_line.reconcile_id.line_id or []) + (
st_line.reconcile_id.line_partial_ids or []):
netsvc.LocalService("workflow").trg_trigger(
=== modified file 'account_banking/account_banking_view.xml'
=== modified file 'account_banking/banking_import_transaction.py'
--- account_banking/banking_import_transaction.py 2012-02-20 19:41:49 +0000
+++ account_banking/banking_import_transaction.py 2012-02-21 12:27:17 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
# -*- encoding: utf-8 -*-
##############################################################################
#
@@ -1855,3 +1856,1873 @@
}
account_bank_statement()
+=======
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
+# Contributions by Kaspars Vilkens (KNdati):
+# lenghty discussions, bugreports and bugfixes
+# Refractoring (C) 2011 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 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 osv import osv, fields
+import time
+import netsvc
+import base64
+import datetime
+from tools import config
+from tools.translate import _
+from parsers import models
+from parsers.convert import *
+# from account_banking.struct import struct
+from account_banking import sepa
+from wizard.banktools import *
+import decimal_precision as dp
+
+bt = models.mem_bank_transaction
+
+class banking_import_transaction(osv.osv):
+ """ orm representation of mem_bank_transaction() for interactive and posthoc
+ configuration of reconciliation in the bank statement view.
+
+ Possible refractoring in OpenERP 6.1:
+ merge with bank_statement_line, using sparse fields
+
+ """
+ _name = 'banking.import.transaction'
+ _description = 'Bank import transaction'
+ _rec_name = '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).
+ # TODO: Convert this to a proper configuration variable
+ payment_window = datetime.timedelta(days=10)
+
+ def _match_costs(self, cr, uid, trans, period_id, account_info, log):
+ '''
+ Get or create a costs invoice for the bank and return it with
+ the payment as seen in the transaction (when not already done).
+ '''
+ if not account_info.costs_account_id:
+ return []
+
+ digits = dp.get_precision('Account')(cr)[1]
+ amount = round(abs(trans.transferred_amount), digits)
+ # Make sure to be able to pinpoint our costs invoice for later
+ # matching
+ reference = '%s.%s: %s' % (trans.statement, trans.transaction, trans.reference)
+
+ # search supplier invoice
+ invoice_obj = self.pool.get('account.invoice')
+ invoice_ids = invoice_obj.search(cr, uid, [
+ '&',
+ ('type', '=', 'in_invoice'),
+ ('partner_id', '=', account_info.bank_partner_id.id),
+ ('company_id', '=', account_info.company_id.id),
+ ('date_invoice', '=', trans.effective_date),
+ ('reference', '=', reference),
+ ('amount_total', '=', amount),
+ ]
+ )
+ if invoice_ids and len(invoice_ids) == 1:
+ invoice = invoice_obj.browse(cr, uid, invoice_ids)[0]
+ elif not invoice_ids:
+ # create supplier invoice
+ partner_obj = self.pool.get('res.partner')
+ invoice_lines = [(0,0,dict(
+ amount = 1,
+ price_unit = amount,
+ name = trans.message or trans.reference,
+ account_id = account_info.costs_account_id.id
+ ))]
+ invoice_address_id = partner_obj.address_get(
+ cr, uid, [account_info.bank_partner_id.id], ['invoice']
+ )
+ invoice_id = invoice_obj.create(cr, uid, dict(
+ type = 'in_invoice',
+ company_id = account_info.company_id.id,
+ partner_id = account_info.bank_partner_id.id,
+ address_invoice_id = invoice_address_id['invoice'],
+ period_id = period_id,
+ journal_id = account_info.invoice_journal_id.id,
+ account_id = account_info.bank_partner_id.property_account_payable.id,
+ date_invoice = trans.effective_date,
+ reference_type = 'none',
+ reference = reference,
+ name = trans.reference or trans.message,
+ check_total = amount,
+ invoice_line = invoice_lines,
+ ))
+ invoice = invoice_obj.browse(cr, uid, invoice_id)
+ # Create workflow
+ invoice_obj.button_compute(cr, uid, [invoice_id],
+ {'type': 'in_invoice'}, set_total=True)
+ wf_service = netsvc.LocalService('workflow')
+ # Move to state 'open'
+ wf_service.trg_validate(uid, 'account.invoice', invoice.id,
+ 'invoice_open', cr)
+
+ # return move_lines to mix with the rest
+ return [x for x in invoice.move_id.line_id if x.account_id.reconcile]
+
+ def _match_debit_order(
+ self, cr, uid, trans, log, context=None):
+
+ def is_zero(total):
+ return self.pool.get('res.currency').is_zero(
+ cr, uid, trans.statement_id.currency, total)
+
+ payment_order_obj = self.pool.get('payment.order')
+ order_ids = payment_order_obj.search(
+ cr, uid, [('payment_order_type', '=', 'debit'),
+ ('state', '=', 'sent'),
+ ('date_sent', '<=', str2date(trans.execution_date,
+ '%Y-%m-%d'))
+ ],
+ limit=0, context=context)
+ orders = payment_order_obj.browse(cr, uid, order_ids, context)
+ candidates = [x for x in orders if
+ is_zero(x.total - trans.transferred_amount)]
+ if len(candidates) > 0:
+ # retrieve the common account_id, if any
+ account_id = False
+ for line in candidates[0].line_ids[0].debit_move_line_id.move_id.line_id:
+ if line.account_id.type == 'other':
+ account_id = line.account_id.id
+ break
+ return dict(
+ move_line_ids = False,
+ match_type = 'payment_order',
+ payment_order_ids = [x.id for x in candidates],
+ account_id = account_id,
+ partner_id = False,
+ partner_bank_id = False,
+ reference = False,
+ type='general',
+ )
+ return False
+
+ def _match_invoice(self, cr, uid, trans, move_lines,
+ partner_ids, bank_account_ids,
+ log, linked_invoices,
+ context=None):
+ '''
+ Find the invoice belonging to this reference - if there is one
+ Use the sales journal to check.
+
+ Challenges we're facing:
+ 1. The sending or receiving party is not necessarily the same as the
+ partner the payment relates to.
+ 2. References can be messed up during manual encoding and inexact
+ matching can link the wrong invoices.
+ 3. Amounts can or can not match the expected amount.
+ 4. Multiple invoices can be paid in one transaction.
+ .. There are countless more, but these we'll try to address.
+
+ Assumptions for matching:
+ 1. There are no payments for invoices not sent. These are dealt with
+ later on.
+ 2. Debit amounts are either customer invoices or credited supplier
+ invoices.
+ 3. Credit amounts are either supplier invoices or credited customer
+ invoices.
+ 4. Payments are either below expected amount or only slightly above
+ (abs).
+ 5. Payments from partners that are matched, pay their own invoices.
+
+ Worst case scenario:
+ 1. No match was made.
+ No harm done. Proceed with manual matching as usual.
+ 2. The wrong match was made.
+ Statements are encoded in draft. You will have the opportunity to
+ manually correct the wrong assumptions.
+
+ TODO: REVISE THIS DOC
+ #Return values:
+ # old_trans: this function can modify and rebrowse the modified
+ # transaction.
+ # move_info: the move_line information belonging to the matched
+ # invoice
+ # new_trans: the new transaction when the current one was split.
+ # This can happen when multiple invoices were paid with a single
+ # bank transaction.
+ '''
+
+ def eyecatcher(invoice):
+ '''
+ Return the eyecatcher for an invoice
+ '''
+ return invoice.type.startswith('in_') and invoice.name or \
+ invoice.number
+
+ def has_id_match(invoice, ref, msg):
+ '''
+ Aid for debugging - way more comprehensible than complex
+ comprehension filters ;-)
+
+ Match on ID of invoice (reference, name or number, whatever
+ available and sensible)
+ '''
+ if invoice.reference:
+ # Reference always comes first, as it is manually set for a
+ # reason.
+ iref = invoice.reference.upper()
+ if iref in ref or iref in msg:
+ return True
+ if invoice.type.startswith('in_'):
+ # Internal numbering, no likely match on number
+ if invoice.name:
+ iname = invoice.name.upper()
+ if iname in ref or iname in msg:
+ return True
+ elif invoice.type.startswith('out_'):
+ # External id's possible and likely
+ inum = invoice.number.upper()
+ if inum in ref or inum in msg:
+ return True
+
+ return False
+
+ def _cached(move_line):
+ # Disabled, we allow for multiple matches in
+ # the interactive wizard
+ return False
+
+ '''Check if the move_line has been cached'''
+ return move_line.id in linked_invoices
+
+ def _cache(move_line, remaining=0.0):
+ '''Cache the move_line'''
+ linked_invoices[move_line.id] = remaining
+
+ def _remaining(move_line):
+ '''Return the remaining amount for a previously matched move_line
+ '''
+ return linked_invoices[move_line.id]
+
+ def _sign(invoice):
+ '''Return the direction of an invoice'''
+ return {'in_invoice': -1,
+ 'in_refund': 1,
+ 'out_invoice': 1,
+ 'out_refund': -1
+ }[invoice.type]
+
+ def is_zero(move_line, total):
+ return self.pool.get('res.currency').is_zero(
+ cr, uid, trans.statement_id.currency, total)
+
+ digits = dp.get_precision('Account')(cr)[1]
+ partial = False
+
+ # Disabled splitting transactions for now
+ # TODO allow splitting in the interactive wizard
+ allow_splitting = False
+
+ # Search invoice on partner
+ if partner_ids:
+ candidates = [
+ x for x in move_lines
+ if x.partner_id.id in partner_ids and
+ (str2date(x.date, '%Y-%m-%d') <=
+ (str2date(trans.execution_date, '%Y-%m-%d') +
+ self.payment_window))
+ and (not _cached(x) or _remaining(x))
+ ]
+ else:
+ candidates = []
+
+ # Next on reference/invoice number. Mind that this uses the invoice
+ # itself, as the move_line references have been fiddled with on invoice
+ # creation. This also enables us to search for the invoice number in the
+ # reference instead of the other way around, as most human interventions
+ # *add* text.
+ if len(candidates) > 1 or not candidates:
+ ref = trans.reference.upper()
+ msg = trans.message.upper()
+ # The manual usage of the sales journal creates moves that
+ # are not tied to invoices. Thanks to Stefan Rijnhart for
+ # reporting this.
+ candidates = [
+ x for x in candidates or move_lines
+ if (x.invoice and has_id_match(x.invoice, ref, msg) and
+ str2date(x.invoice.date_invoice, '%Y-%m-%d') <=
+ (str2date(trans.execution_date, '%Y-%m-%d') +
+ self.payment_window)
+ and (not _cached(x) or _remaining(x)))
+ ]
+
+ # Match on amount expected. Limit this kind of search to known
+ # partners.
+ if not candidates and partner_ids:
+ candidates = [
+ x for x in move_lines
+ if (is_zero(x.move_id, ((x.debit or 0.0) - (x.credit or 0.0)) -
+ trans.transferred_amount)
+ and str2date(x.date, '%Y-%m-%d') <=
+ (str2date(trans.execution_date, '%Y-%m-%d') +
+ self.payment_window)
+ and (not _cached(x) or _remaining(x)))
+ ]
+
+ move_line = False
+
+ if candidates and len(candidates) > 0:
+ # Now a possible selection of invoices has been found, check the
+ # amounts expected and received.
+ #
+ # TODO: currency coercing
+ best = [x for x in candidates
+ if (is_zero(x.move_id, ((x.debit or 0.0) - (x.credit or 0.0)) -
+ trans.transferred_amount)
+ and str2date(x.date, '%Y-%m-%d') <=
+ (str2date(trans.execution_date, '%Y-%m-%d') +
+ self.payment_window))
+ ]
+ if len(best) == 1:
+ # Exact match
+ move_line = best[0]
+ invoice = move_line.invoice
+ if _cached(move_line):
+ partial = True
+ expected = _remaining(move_line)
+ else:
+ _cache(move_line)
+
+ elif len(candidates) > 1:
+ # Before giving up, check cache for catching duplicate
+ # transfers first
+ paid = [x for x in move_lines
+ if x.invoice and has_id_match(x.invoice, ref, msg)
+ and str2date(x.invoice.date_invoice, '%Y-%m-%d')
+ <= str2date(trans.execution_date, '%Y-%m-%d')
+ and (_cached(x) and not _remaining(x))
+ ]
+ if paid:
+ log.append(
+ _('Unable to link transaction id %(trans)s '
+ '(ref: %(ref)s) to invoice: '
+ 'invoice %(invoice)s was already paid') % {
+ 'trans': '%s.%s' % (trans.statement, trans.transaction),
+ 'ref': trans.reference,
+ 'invoice': eyecatcher(paid[0].invoice)
+ })
+ else:
+ # Multiple matches
+ # TODO select best bank account in this case
+ return (trans, self._get_move_info(
+ cr, uid, [x.id for x in candidates]),
+ False)
+ move_line = False
+ partial = False
+
+ elif len(candidates) == 1:
+ # Mismatch in amounts
+ move_line = candidates[0]
+ invoice = move_line.invoice
+ expected = round(_sign(invoice) * invoice.residual, digits)
+ partial = True
+
+ trans2 = None
+ if move_line and partial:
+ found = round(trans.transferred_amount, digits)
+ if abs(expected) == abs(found):
+ partial = False
+ # Last partial payment will not flag invoice paid without
+ # manual assistence
+ # Stefan: disabled this here for the interactive method
+ # Handled this with proper handling of partial reconciliation
+ # and the workflow service
+ # invoice_obj = self.pool.get('account.invoice')
+ # invoice_obj.write(cr, uid, [invoice.id], {
+ # 'state': 'paid'
+ # })
+ elif abs(expected) > abs(found):
+ # Partial payment, reuse invoice
+ _cache(move_line, expected - found)
+ elif abs(expected) < abs(found) and allow_splitting:
+ # Possible combined payments, need to split transaction to
+ # verify
+ _cache(move_line)
+ trans2 = self.copy(
+ cr, uid, trans.id,
+ dict(
+ transferred_amount = trans.transferred_amount - expected,
+ transaction = trans.transaction + 'b',
+ parent_id = trans.id,
+ ), context=context)
+ # update the current record
+ self.write(cr, uid, trans.id, dict(
+ transferred_amount = expected,
+ transaction = trans.transaction + 'a',
+ ), context)
+ # rebrowse the current record after writing
+ trans = self.browse(cr, uid, trans.id, context=context)
+ if move_line:
+ account_ids = [
+ x.id for x in bank_account_ids
+ if x.partner_id.id == move_line.partner_id.id
+ ]
+
+ return (trans, self._get_move_info(
+ cr, uid, [move_line.id],
+ account_ids and account_ids[0] or False),
+ trans2)
+
+ return trans, False, False
+
+ def _do_move_reconcile(
+ self, cr, uid, move_line_ids, currency, amount, context=None):
+ """
+ Prepare a reconciliation for a bank transaction of the given
+ amount. The caller MUST add the move line associated with the
+ bank transaction to the returned reconciliation resource.
+
+ If adding the amount does not make the total add up,
+ prepare a partial reconciliation. An existing reconciliation on
+ the move lines will be taken into account.
+
+ :param move_line_ids: List of ids. This will usually be the move
+ line of an associated invoice or payment, plus optionally the
+ move line of a writeoff.
+ :param currency: A res.currency *browse* object to perform math
+ operations on the amounts.
+ :param amount: the amount of the bank transaction. Amount < 0 in
+ case of a credit move on the bank account.
+ """
+ move_line_obj = self.pool.get('account.move.line')
+ reconcile_obj = self.pool.get('account.move.reconcile')
+ is_zero = lambda amount: self.pool.get('res.currency').is_zero(
+ cr, uid, currency, amount)
+ move_lines = move_line_obj.browse(cr, uid, move_line_ids, context=context)
+ reconcile = False
+ for move_line in move_lines:
+ if move_line.reconcile_id:
+ raise osv.except_osv(
+ _('Entry is already reconciled'),
+ _("You cannot reconcile the bank transaction with this entry, " +
+ "it is already reconciled")
+ )
+ if move_line.reconcile_partial_id:
+ if reconcile and reconcile.id != move_line.reconcile_partial_id.id:
+ raise osv.except_osv(
+ _('Cannot reconcile'),
+ _('Move lines are already partially reconciled, ' +
+ 'but not with each other.'))
+ reconcile = move_line.reconcile_partial_id
+ line_ids = list(set(move_line_ids + (
+ [x.id for x in reconcile and ( # reconcile.line_id or
+ reconcile.line_partial_ids) or []])))
+ if not reconcile:
+ reconcile_id = reconcile_obj.create(
+ cr, uid, {'type': 'auto' }, context=context)
+ reconcile = reconcile_obj.browse(cr, uid, reconcile_id, context=context)
+ full = is_zero(
+ move_line_obj.get_balance(cr, uid, line_ids) - amount)
+ # we should not have to check whether there is a surplus writeoff
+ # as any surplus amount *should* have been split off in the matching routine
+ if full:
+ line_partial_ids = []
+ else:
+ line_partial_ids = line_ids[:]
+ line_ids = []
+ reconcile_obj.write(
+ cr, uid, reconcile.id,
+ { 'line_id': [(6, 0, line_ids)],
+ 'line_partial_ids': [(6, 0, line_partial_ids)],
+ }, context=context)
+ return reconcile.id
+
+ def _do_move_unreconcile(self, cr, uid, move_line_ids, currency, context=None):
+ """
+ Undo a reconciliation, removing the given move line ids. If no
+ meaningful (partial) reconciliation remains, delete it.
+
+ :param move_line_ids: List of ids. This will usually be the move
+ line of an associated invoice or payment, plus optionally the
+ move line of a writeoff.
+ :param currency: A res.currency *browse* object to perform math
+ operations on the amounts.
+ """
+
+ move_line_obj = self.pool.get('account.move.line')
+ reconcile_obj = self.pool.get('account.move.reconcile')
+ is_zero = lambda amount: self.pool.get('res.currency').is_zero(
+ cr, uid, currency, amount)
+ move_lines = move_line_obj.browse(cr, uid, move_line_ids, context=context)
+ reconcile = move_lines[0].reconcile_id or move_lines[0].reconcile_partial_id
+ line_ids = [x.id for x in reconcile.line_id or reconcile.line_partial_ids]
+ for move_line_id in move_line_ids:
+ line_ids.remove(move_line_id)
+ if len(line_ids) > 1:
+ full = is_zero(move_line_obj.get_balance(cr, uid, line_ids))
+ if full:
+ line_partial_ids = []
+ else:
+ line_partial_ids = list(line_ids)
+ line_ids = []
+ reconcile_obj.write(
+ cr, uid, reconcile.id,
+ { 'line_partial_ids': [(6, 0, line_partial_ids)],
+ 'line_id': [(6, 0, line_ids)],
+ }, context=context)
+ else:
+ reconcile_obj.unlink(cr, uid, reconcile.id, context=context)
+ for move_line in move_lines:
+ if move_line.invoice:
+ # reopening the invoice
+ netsvc.LocalService('workflow').trg_validate(
+ uid, 'account.invoice', move_line.invoice.id, 'undo_paid', cr)
+ return True
+
+ def _reconcile_move(
+ self, cr, uid, transaction_id, context=None):
+ transaction = self.browse(cr, uid, transaction_id, context=context)
+ if not transaction.move_line_id:
+ if transaction.match_type == 'invoice':
+ raise osv.except_osv(
+ _("Cannot link transaction %s with invoice") %
+ transaction.statement_line_id.name,
+ (transaction.invoice_ids and
+ (_("Please select one of the matches in transaction %s.%s") or
+ _("No match found for transaction %s.%s")) % (
+ transaction.statement_line_id.statement_id.name,
+ transaction.statement_line_id.name
+ )))
+ else:
+ raise osv.except_osv(
+ _("Cannot link transaction %s with accounting entry") %
+ transaction.statement_line_id.name,
+ (transaction.move_line_ids and
+ (_("Please select one of the matches in transaction %s.%s") or
+ _("No match found for transaction %s.%s")) % (
+ transaction.statement_line_id.statement_id.name,
+ transaction.statement_line_id.name
+ )))
+ currency = transaction.statement_line_id.statement_id.currency
+ line_ids = [transaction.move_line_id.id]
+ if transaction.writeoff_move_line_id:
+ line_ids.append(transaction.writeoff_move_line_id.id)
+ reconcile_id = self._do_move_reconcile(
+ cr, uid, line_ids, currency,
+ transaction.transferred_amount, context=context)
+ return reconcile_id
+
+ def _reconcile_storno(
+ self, cr, uid, transaction_id, context=None):
+ """
+ Creation of the reconciliation has been delegated to
+ *a* direct debit module, to allow for various direct debit styles
+ """
+ payment_line_obj = self.pool.get('payment.line')
+ transaction = self.browse(cr, uid, transaction_id, context=context)
+ if not transaction.payment_line_id:
+ raise osv.except_osv(
+ _("Cannot link with storno"),
+ _("No direct debit order item"))
+ return payment_line_obj.debit_storno(
+ cr, uid,
+ transaction.payment_line_id.id,
+ transaction.statement_line_id.amount,
+ transaction.statement_line_id.currency,
+ transaction.storno_retry,
+ context=context)
+
+ def _reconcile_payment_order(
+ self, cr, uid, transaction_id, context=None):
+ """
+ Creation of the reconciliation has been delegated to
+ *a* direct debit module, to allow for various direct debit styles
+ """
+ payment_order_obj = self.pool.get('payment.order')
+ transaction = self.browse(cr, uid, transaction_id, context=context)
+ if not transaction.payment_order_id:
+ raise osv.except_osv(
+ _("Cannot reconcile"),
+ _("Cannot reconcile: no direct debit order"))
+ if transaction.payment_order_id.payment_order_type != 'debit':
+ raise osv.except_osv(
+ _("Cannot reconcile"),
+ _("Reconcile payment order not implemented"))
+ return payment_order_obj.debit_reconcile_transfer(
+ cr, uid,
+ transaction.payment_order_id.id,
+ transaction.statement_line_id.amount,
+ transaction.statement_line_id.currency,
+ context=context)
+
+ def _reconcile_payment(
+ self, cr, uid, transaction_id, context=None):
+ """
+ Do some housekeeping on the payment line
+ then pass on to _reconcile_move
+ """
+ transaction = self.browse(cr, uid, transaction_id, context=context)
+ payment_line_obj = self.pool.get('payment.line')
+ payment_line_obj.write(
+ cr, uid, transaction.payment_line_id.id, {
+ 'export_state': 'done',
+ 'date_done': transaction.effective_date,
+ }
+ )
+ return self._reconcile_move(cr, uid, transaction_id, context=context)
+
+ def _cancel_payment(
+ self, cr, uid, transaction_id, context=None):
+ raise osv.except_osv(
+ _("Cannot unreconcile"),
+ _("Cannot unreconcile: this operation is not yet supported for "
+ "match type 'payment'"))
+
+ def _cancel_payment_order(
+ self, cr, uid, transaction_id, context=None):
+ """
+ """
+ payment_order_obj = self.pool.get('payment.order')
+ transaction = self.browse(cr, uid, transaction_id, context=context)
+ if not transaction.payment_order_id:
+ raise osv.except_osv(
+ _("Cannot unreconcile"),
+ _("Cannot unreconcile: no direct debit order"))
+ if transaction.payment_order_id.payment_order_type != 'debit':
+ raise osv.except_osv(
+ _("Cannot unreconcile"),
+ _("Unreconcile payment order not implemented"))
+ return payment_order_obj.debit_unreconcile_transfer(
+ cr, uid, transaction.payment_order_id.id,
+ transaction.statement_line_id.reconcile_id.id,
+ transaction.statement_line_id.amount,
+ transaction.statement_line_id.currency)
+
+ def _cancel_move(
+ self, cr, uid, transaction_id, context=None):
+ """
+ Undo the reconciliation of a transaction with a move line
+ in the system: Retrieve the move line from the bank statement line's
+ move that is reconciled with the matching move line recorded
+ on the transaction. Do not actually remove the latter from the
+ reconciliation, as it may be further reconciled.
+ Unreconcile the bank statement move line and the optional
+ write-off move line
+ """
+ statement_line_obj = self.pool.get('account.bank.statement.line')
+ transaction = self.browse(cr, uid, transaction_id, context=context)
+ currency = transaction.statement_line_id.statement_id.currency
+ reconcile_id = (
+ transaction.move_line_id.reconcile_id and
+ transaction.move_line_id.reconcile_id.id or
+ transaction.move_line_id.reconcile_partial_id and
+ transaction.move_line_id.reconcile_partial_id.id
+ )
+ for line in transaction.statement_line_id.move_id.line_id:
+ line_reconcile = line.reconcile_id or line.reconcile_partial_id
+ if line_reconcile and line_reconcile.id == reconcile_id:
+ st_line_line = line
+ break
+ line_ids = [st_line_line.id]
+ # Add the write off line
+ if transaction.writeoff_move_line_id:
+ line_ids.append(transaction.writeoff_move_line_id.id)
+ self._do_move_unreconcile(
+ cr, uid, line_ids, currency, context=context)
+ statement_line_obj.write(
+ cr, uid, transaction.statement_line_id.id,
+ {'reconcile_id': False}, context=context)
+ return True
+
+ def _cancel_storno(
+ self, cr, uid, transaction_id, context=None):
+ """
+ TODO: delegate unreconciliation to the direct debit module,
+ to allow for various direct debit styles
+ """
+ payment_line_obj = self.pool.get('payment.line')
+ reconcile_obj = self.pool.get('account.move.reconcile')
+ transaction = self.browse(cr, uid, transaction_id, context=context)
+
+ if not transaction.payment_line_id:
+ raise osv.except_osv(
+ _("Cannot cancel link with storno"),
+ _("No direct debit order item"))
+ if not transaction.payment_line_id.storno:
+ raise osv.except_osv(
+ _("Cannot cancel link with storno"),
+ _("The direct debit order item is not marked for storno"))
+
+ journal = transaction.statement_line_id.statement_id.journal_id
+ if transaction.statement_line_id.amount >= 0:
+ account_id = journal.default_credit_account_id.id
+ else:
+ account_id = journal.default_debit_account_id.id
+ cancel_line = False
+ for line in transaction.statement_line_id.move_id.line_id:
+ if line.account_id.id != account_id:
+ cancel_line = line
+ break
+ if not cancel_line: # debug
+ raise osv.except_osv(
+ _("Cannot cancel link with storno"),
+ _("Line id not found"))
+ reconcile = cancel_line.reconcile_id or cancel_line.reconcile_partial_id
+ lines_reconcile = reconcile.line_id or reconcile.line_partial_ids
+ if len(lines_reconcile) < 3:
+ # delete the full reconciliation
+ reconcile_obj.unlink(cr, uid, reconcile.id, context)
+ else:
+ # we are left with a partial reconciliation
+ reconcile_obj.write(
+ cr, uid, reconcile.id,
+ {'line_partial_ids':
+ [(6, 0, [x.id for x in lines_reconcile if x.id != cancel_line.id])],
+ 'line_id': [(6, 0, [])],
+ }, context)
+ # redo the original payment line reconciliation with the invoice
+ payment_line_obj.write(
+ cr, uid, transaction.payment_line_id.id,
+ {'storno': False}, context)
+ payment_line_obj.debit_reconcile(
+ cr, uid, transaction.payment_line_id.id, context)
+
+ cancel_map = {
+ 'storno': _cancel_storno,
+ 'invoice': _cancel_move,
+ 'manual': _cancel_move,
+ 'move': _cancel_move,
+ 'payment_order': _cancel_payment_order,
+ 'payment': _cancel_payment,
+ }
+ def cancel(self, cr, uid, ids, context=None):
+ if ids and isinstance(ids, (int, float)):
+ ids = [ids]
+ move_obj = self.pool.get('account.move')
+ for transaction in self.browse(cr, uid, ids, context):
+ if not transaction.match_type:
+ continue
+ if transaction.match_type not in self.cancel_map:
+ raise osv.except_osv(
+ _("Cannot cancel type %s" % transaction.match_type),
+ _("No method found to cancel this type"))
+ self.cancel_map[transaction.match_type](
+ self, cr, uid, transaction.id, context)
+ # clear up the writeoff move
+ if transaction.writeoff_move_line_id:
+ move_obj.button_cancel(
+ cr, uid, [transaction.writeoff_move_line_id.move_id.id],
+ context=context)
+ move_obj.unlink(
+ cr, uid, [transaction.writeoff_move_line_id.move_id.id],
+ context=context)
+ return True
+
+ reconcile_map = {
+ 'storno': _reconcile_storno,
+ 'invoice': _reconcile_move,
+ 'manual': _reconcile_move,
+ 'payment_order': _reconcile_payment_order,
+ 'payment': _reconcile_payment,
+ 'move': _reconcile_move,
+ }
+ def reconcile(self, cr, uid, ids, context=None):
+ if ids and isinstance(ids, (int, float)):
+ ids = [ids]
+ for transaction in self.browse(cr, uid, ids, context):
+ if not transaction.match_type:
+ continue
+ if transaction.match_type not in self.reconcile_map:
+ raise osv.except_osv(
+ _("Cannot reconcile"),
+ _("Cannot reconcile type %s. No method found to " +
+ "reconcile this type") %
+ transaction.match_type
+ )
+ if (transaction.residual and transaction.writeoff_account_id):
+ if transaction.match_type not in ('invoice', 'move', 'manual'):
+ raise osv.except_osv(
+ _("Cannot reconcile"),
+ _("Bank transaction %s: write off not implemented for " +
+ "this match type.") %
+ transaction.statement_line_id.name
+ )
+ self._generate_writeoff_move(
+ cr, uid, transaction.id, context=context)
+ # run the method that is appropriate for this match type
+ reconcile_id = self.reconcile_map[transaction.match_type](
+ self, cr, uid, transaction.id, context)
+ self.pool.get('account.bank.statement.line').write(
+ cr, uid, transaction.statement_line_id.id,
+ {'reconcile_id': reconcile_id}, context=context)
+
+ # TODO
+ # update the statement line bank account reference
+ # as follows (from _match_invoice)
+
+ """
+ account_ids = [
+ x.id for x in bank_account_ids
+ if x.partner_id.id == move_line.partner_id.id
+ ][0]
+ """
+ return True
+
+
+ def _generate_writeoff_move(self, cr, uid, ids, context):
+ if ids and isinstance(ids, (int, float)):
+ ids = [ids]
+ move_line_obj = self.pool.get('account.move.line')
+ move_obj = self.pool.get('account.move')
+ for trans in self.browse(cr, uid, ids, context=context):
+ periods = self.pool.get('account.period').find(
+ cr, uid, trans.statement_line_id.date)
+ period_id = periods and periods[0] or False
+ move_id = move_obj.create(cr, uid, {
+ 'journal_id': trans.writeoff_journal_id.id,
+ 'period_id': period_id,
+ 'date': trans.statement_line_id.date,
+ 'name': '(write-off) %s' % (
+ trans.move_line_id.move_id.name or '')
+ }, context=context)
+ if trans.residual > 0:
+ writeoff_debit = trans.residual
+ writeoff_credit = False
+ else:
+ writeoff_debit = False
+ writeoff_credit = - trans.residual
+ vals = {
+ 'name': trans.statement_line_id.name,
+ 'date': trans.statement_line_id.date,
+ 'ref': trans.statement_line_id.ref,
+ 'move_id': move_id,
+ 'partner_id': (trans.statement_line_id.partner_id and
+ trans.statement_line_id.partner_id.id or False),
+ 'account_id': trans.statement_line_id.account_id.id,
+ 'credit': writeoff_debit,
+ 'debit': writeoff_credit,
+ 'journal_id': trans.writeoff_journal_id.id,
+ 'period_id': period_id,
+ 'currency_id': trans.statement_line_id.statement_id.currency.id,
+ }
+ move_line_id = move_line_obj.create(
+ cr, uid, vals, context=context)
+ self.write(
+ cr, uid, trans.id,
+ {'writeoff_move_line_id': move_line_id}, context=context)
+ vals.update({
+ 'account_id': trans.writeoff_account_id.id,
+ 'credit': writeoff_credit,
+ 'debit': writeoff_debit,
+ })
+ move_line_obj.create(
+ cr, uid, vals, context=context)
+ move_obj.post(
+ cr, uid, [move_id], context=context)
+
+ def _match_storno(
+ self, cr, uid, trans, log, context=None):
+ payment_line_obj = self.pool.get('payment.line')
+ line_ids = payment_line_obj.search(
+ cr, uid, [
+ ('order_id.payment_order_type', '=', 'debit'),
+ ('order_id.state', 'in', ['sent', 'done']),
+ ('communication', '=', trans.reference)
+ ], context=context)
+ # stornos MUST have an exact match
+ if len(line_ids) == 1:
+ account_id = payment_line_obj.get_storno_account_id(
+ cr, uid, line_ids[0], trans.transferred_amount,
+ trans.statement_id.currency, context=None)
+ if account_id:
+ return dict(
+ account_id = account_id,
+ match_type = 'storno',
+ payment_line_id = line_ids[0],
+ move_line_ids=False,
+ partner_id=False,
+ partner_bank_id=False,
+ reference=False,
+ type='customer',
+ )
+ # TODO log the reason why there is no result for transfers marked
+ # as storno
+ return False
+
+ def _match_payment(self, cr, uid, trans, payment_lines,
+ partner_ids, bank_account_ids, log, linked_payments):
+ '''
+ Find the payment order belonging to this reference - if there is one
+ This is the easiest part: when sending payments, the returned bank info
+ should be identical to ours.
+ This also means that we do not allow for multiple candidates.
+ '''
+ # TODO: Not sure what side effects are created when payments are done
+ # for credited customer invoices, which will be matched later on too.
+ digits = dp.get_precision('Account')(cr)[1]
+ candidates = [x for x in payment_lines
+ if x.communication == trans.reference
+ and round(x.amount, digits) == -round(trans.transferred_amount, digits)
+ and trans.remote_account in (x.bank_id.acc_number,
+ x.bank_id.iban)
+ ]
+ if len(candidates) == 1:
+ candidate = candidates[0]
+ # Check cache to prevent multiple matching of a single payment
+ if candidate.id not in linked_payments:
+ linked_payments[candidate.id] = True
+ move_info = self._get_move_info(cr, uid, [candidate.move_line_id.id])
+ move_info.update({
+ 'match_type': 'payment',
+ 'payment_line_id': candidate.id,
+ })
+ return move_info
+
+ return False
+
+ signal_duplicate_keys = [
+ # does not include float values
+ # such as transferred_amount
+ 'execution_date', 'local_account', 'remote_account',
+ 'remote_owner', 'reference', 'message',
+ ]
+
+ def create(self, cr, uid, vals, context=None):
+ res = super(banking_import_transaction, self).create(
+ cr, uid, vals, context)
+ if res:
+ me = self.browse(cr, uid, res, context)
+ search_vals = [(key, '=', me[key])
+ for key in self.signal_duplicate_keys]
+ ids = self.search(cr, uid, search_vals, context=context)
+ dupes = []
+ # Test for transferred_amount seperately
+ # due to float representation and rounding difficulties
+ for trans in self.browse(cr, uid, ids, context=context):
+ if self.pool.get('res.currency').is_zero(
+ cr, uid,
+ trans.statement_id.currency,
+ me['transferred_amount'] - trans.transferred_amount):
+ dupes.append(trans.id)
+ if len(dupes) < 1:
+ raise osv.except_osv(_('Cannot check for duplicate'),
+ _("Cannot check for duplicate. "
+ "I can't find myself."))
+ if len(dupes) > 1:
+ self.write(
+ cr, uid, res, {'duplicate': True}, context=context)
+ return res
+
+ def split_off(self, cr, uid, res_id, amount, context=None):
+ # todo. Inherit the duplicate marker from res_id
+ pass
+
+ def combine(self, cr, uid, ids, context=None):
+ # todo. Check equivalence of primary key
+ pass
+
+ def _get_move_info(self, cr, uid, move_line_ids, partner_bank_id=False,
+ partial=False, match_type = False):
+ type_map = {
+ 'out_invoice': 'customer',
+ 'in_invoice': 'supplier',
+ 'out_refund': 'customer',
+ 'in_refund': 'supplier',
+ }
+ retval = {'partner_id': False,
+ 'partner_bank_id': partner_bank_id,
+ 'reference': False,
+ 'type': 'general',
+ 'move_line_ids': move_line_ids,
+ 'match_type': match_type,
+ 'account_id': False,
+ }
+ move_lines = self.pool.get('account.move.line').browse(cr, uid, move_line_ids)
+ for move_line in move_lines:
+ if move_line.partner_id:
+ if retval['partner_id']:
+ if retval['partner_id'] != move_line.partner_id.id:
+ retval['partner_id'] = False
+ break
+ else:
+ retval['partner_id'] = move_line.partner_id.id
+ else:
+ if retval['partner_id']:
+ retval['partner_id'] = False
+ break
+ for move_line in move_lines:
+ if move_line.account_id:
+ if retval['account_id']:
+ if retval['account_id'] != move_line.account_id.id:
+ retval['account_id'] = False
+ break
+ else:
+ retval['account_id'] = move_line.account_id.id
+ else:
+ if retval['account_id']:
+ retval['account_id'] = False
+ break
+ for move_line in move_lines:
+ if move_line.invoice:
+ if retval['match_type']:
+ if retval['match_type'] != 'invoice':
+ retval['match_type'] = False
+ break
+ else:
+ retval['match_type'] = 'invoice'
+ else:
+ if retval['match_type']:
+ retval['match_type'] = False
+ break
+ if move_lines and not retval['match_type']:
+ retval['match_type'] = 'move'
+ if move_lines and len(move_lines) == 1:
+ retval['reference'] = move_lines[0].ref
+ if retval['match_type'] == 'invoice':
+ retval['invoice_ids'] = [x.invoice.id for x in move_lines]
+ retval['type'] = type_map[move_lines[0].invoice.type]
+ return retval
+
+ def match(self, cr, uid, ids, results=None, context=None):
+ if not ids:
+ return True
+
+ company_obj = self.pool.get('res.company')
+ partner_bank_obj = self.pool.get('res.partner.bank')
+ journal_obj = self.pool.get('account.journal')
+ move_line_obj = self.pool.get('account.move.line')
+ payment_line_obj = self.pool.get('payment.line')
+ statement_line_obj = self.pool.get('account.bank.statement.line')
+ statement_obj = self.pool.get('account.bank.statement')
+ payment_order_obj = self.pool.get('payment.order')
+ imported_statement_ids = []
+
+ # Results
+ if results is None:
+ results = dict(
+ trans_loaded_cnt = 0,
+ trans_skipped_cnt = 0,
+ trans_matched_cnt = 0,
+ bank_costs_invoice_cnt = 0,
+ error_cnt = 0,
+ log = [],
+ )
+
+ # Caching
+ error_accounts = {}
+ info = {}
+ linked_payments = {}
+ # TODO: harvest linked invoices from draft statement lines?
+ linked_invoices = {}
+ payment_lines = []
+
+ # Get all unreconciled sent payment lines in one big swoop.
+ # No filtering can be done, as empty dates carry value for C2B
+ # communication. Most likely there are much less sent payments
+ # than reconciled and open/draft payments.
+ # Strangely, payment_orders still do not have company_id
+ cr.execute("SELECT l.id FROM payment_order o, payment_line l "
+ "WHERE l.order_id = o.id AND "
+ "o.state = 'sent' AND "
+ "l.date_done IS NULL"
+ )
+ payment_line_ids = [x[0] for x in cr.fetchall()]
+ if payment_line_ids:
+ payment_lines = payment_line_obj.browse(cr, uid, payment_line_ids)
+
+ # Start the loop over the transactions requested to match
+ transactions = self.browse(cr, uid, ids, context)
+ # TODO: do we do injected transactions here?
+ injected = []
+ i = 0
+ max_trans = len(transactions)
+ while i < max_trans:
+ move_info = False
+ if injected:
+ # Force FIFO behavior
+ transaction = injected.pop(0)
+ else:
+ transaction = transactions[i]
+
+ if (transaction.statement_line_id and
+ transaction.statement_line_id.state == 'confirmed'):
+ raise osv.except_osv(
+ _("Cannot perform match"),
+ _("Cannot perform match on a confirmed transction"))
+
+ if transaction.local_account in error_accounts:
+ results['trans_skipped_cnt'] += 1
+ if not injected:
+ i += 1
+ continue
+
+ # TODO: optimize by ordering transactions per company,
+ # and perform the stanza below only once per company.
+ # In that case, take newest transaction date into account
+ # when retrieving move_line_ids below.
+ company = company_obj.browse(
+ cr, uid, transaction.company_id.id, context)
+ # Get default defaults
+ def_pay_account_id = company.partner_id.property_account_payable.id
+ def_rec_account_id = company.partner_id.property_account_receivable.id
+
+ # Get interesting journals once
+ # Added type 'general' to capture fund transfers
+ journal_ids = journal_obj.search(cr, uid, [
+ ('type', 'in', ('general', 'sale','purchase',
+ 'purchase_refund','sale_refund')),
+ ('company_id', '=', company.id),
+ ])
+ # Get all unreconciled moves
+ move_line_ids = move_line_obj.search(cr, uid, [
+ ('reconcile_id', '=', False),
+ ('journal_id', 'in', journal_ids),
+ ('account_id.reconcile', '=', True),
+ ('date', '<=', transaction.execution_date),
+ ])
+ if move_line_ids:
+ move_lines = move_line_obj.browse(cr, uid, move_line_ids)
+ else:
+ move_lines = []
+
+ # Create fallback currency code
+ currency_code = transaction.local_currency or company.currency_id.name
+
+ # Check cache for account info/currency
+ if transaction.local_account in info and \
+ currency_code in info[transaction.local_account]:
+ account_info = info[transaction.local_account][currency_code]
+ else:
+ # Pull account info/currency
+ account_info = get_company_bank_account(
+ self.pool, cr, uid, transaction.local_account,
+ transaction.local_currency, company, results['log']
+ )
+ if not account_info:
+ results['log'].append(
+ _('Transaction found for unknown account %(bank_account)s') %
+ {'bank_account': transaction.local_account}
+ )
+ error_accounts[transaction.local_account] = True
+ results['error_cnt'] += 1
+ if not injected:
+ i += 1
+ continue
+ if 'journal_id' not in account_info:
+ results['log'].append(
+ _('Transaction found for account %(bank_account)s, '
+ 'but no default journal was defined.'
+ ) % {'bank_account': transaction.local_account}
+ )
+ error_accounts[transaction.local_account] = True
+ results['error_cnt'] += 1
+ if not injected:
+ i += 1
+ continue
+
+ # Get required currency code
+ currency_code = account_info.currency_id.name
+
+ # Cache results
+ if not transaction.local_account in info:
+ info[transaction.local_account] = {
+ currency_code: account_info
+ }
+ else:
+ info[transaction.local_account][currency_code] = account_info
+
+ # Final check: no coercion of currencies!
+ if transaction.local_currency \
+ and account_info.currency_id.name != transaction.local_currency:
+ # TODO: convert currencies?
+ results['log'].append(
+ _('transaction %(statement_id)s.%(transaction_id)s for account %(bank_account)s'
+ ' uses different currency than the defined bank journal.'
+ ) % {
+ 'bank_account': transactions.local_account,
+ 'transaction_id': transaction.statement,
+ 'statement_id': transaction.transaction,
+ }
+ )
+ error_accounts[transaction.local_account] = True
+ results['error_cnt'] += 1
+ if not injected:
+ i += 1
+ continue
+
+ # Link accounting period
+ period_id = get_period(
+ self.pool, cr, uid,
+ str2date(transaction.effective_date,'%Y-%m-%d'), company,
+ results['log'])
+ if not period_id:
+ results['trans_skipped_cnt'] += 1
+ if not injected:
+ i += 1
+ continue
+
+ # When bank costs are part of transaction itself, split it.
+ if transaction.type != bt.BANK_COSTS and transaction.provision_costs:
+ # Create new transaction for bank costs
+ cost_id = self.copy(
+ cr, uid, transaction.id,
+ dict(
+ type = bt.BANK_COSTS,
+ transaction = '%s-prov' % transaction.transaction,
+ transferred_amount = transaction.provision_costs,
+ remote_currency = transaction.provision_costs_currency,
+ message = transaction.provision_costs_description,
+ parent_id = transaction.id,
+ ), context)
+
+ injected.append(self.browse(cr, uid, cost_id, context))
+
+ # Remove bank costs from current transaction
+ # Note that this requires that the transferred_amount
+ # includes the bank costs and that the costs itself are
+ # signed correctly.
+ self.write(
+ cr, uid, transaction.id,
+ dict(
+ transferred_amount =
+ transaction.transferred_amount - transaction.provision_costs,
+ provision_costs = False,
+ provision_costs_currency = False,
+ provision_costs_description = False,
+ ), context=context)
+ # rebrowse the current record after writing
+ transaction=self.browse(cr, uid, transaction.id, context=context)
+ # Match full direct debit orders
+ if transaction.type == bt.DIRECT_DEBIT:
+ move_info = self._match_debit_order(
+ cr, uid, transaction, results['log'], context)
+ if transaction.type == bt.STORNO:
+ move_info = self._match_storno(
+ cr, uid, transaction, results['log'], context)
+ # Allow inclusion of generated bank invoices
+ if transaction.type == bt.BANK_COSTS:
+ lines = self._match_costs(
+ cr, uid, transaction, period_id, account_info,
+ results['log']
+ )
+ results['bank_costs_invoice_cnt'] += bool(lines)
+ for line in lines:
+ if not [x for x in move_lines if x.id == line.id]:
+ move_lines.append(line)
+ partner_ids = [account_info.bank_partner_id.id]
+ partner_banks = []
+ else:
+ # Link remote partner, import account when needed
+ partner_banks = get_bank_accounts(
+ self.pool, cr, uid, transaction.remote_account,
+ results['log'], fail=True
+ )
+ if partner_banks:
+ partner_ids = [x.partner_id.id for x in partner_banks]
+ elif transaction.remote_owner:
+ 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
+ elif company.partner_id and company.partner_id.country:
+ country_code = company.partner_id.country.code
+ else:
+ country_code = None
+ partner_id = get_or_create_partner(
+ self.pool, cr, uid, transaction.remote_owner,
+ transaction.remote_owner_address,
+ transaction.remote_owner_postalcode,
+ transaction.remote_owner_city,
+ country_code, results['log']
+ )
+ if transaction.remote_account:
+ partner_bank_id = create_bank_account(
+ self.pool, cr, uid, partner_id,
+ transaction.remote_account,
+ transaction.remote_owner,
+ transaction.remote_owner_address,
+ transaction.remote_owner_city,
+ country_code, results['log']
+ )
+ partner_banks = partner_bank_obj.browse(
+ cr, uid, [partner_bank_id]
+ )
+ else:
+ partner_bank_id = None
+ partner_banks = []
+ partner_ids = [partner_id]
+ else:
+ partner_ids = []
+ partner_banks = []
+
+ # Credit means payment... isn't it?
+ if (not move_info
+ and transaction.transferred_amount < 0 and payment_lines):
+ # Link open payment - if any
+ move_info = self._match_payment(
+ cr, uid, transaction,
+ payment_lines, partner_ids,
+ partner_banks, results['log'], linked_payments,
+ )
+
+ # Second guess, invoice -> may split transaction, so beware
+ if not move_info:
+ # Link invoice - if any. Although bank costs are not an
+ # invoice, automatic invoicing on bank costs will create
+ # these, and invoice matching still has to be done.
+
+ transaction, move_info, remainder = self._match_invoice(
+ cr, uid, transaction, move_lines, partner_ids,
+ partner_banks, results['log'], linked_invoices,
+ context=context)
+ if remainder:
+ injected.append(self.browse(cr, uid, remainder, context))
+
+ account_id = move_info and move_info.get('account_id', False)
+ if not account_id:
+ # Use the default settings, but allow individual partner
+ # settings to overrule this. Note that you need to change
+ # the internal type of these accounts to either 'payable'
+ # or 'receivable' to enable usage like this.
+ if transaction.transferred_amount < 0:
+ if len(partner_banks) == 1:
+ account_id = (
+ partner_banks[0].partner_id.property_account_payable and
+ partner_banks[0].partner_id.property_account_payable.id)
+ if len(partner_banks) != 1 or not account_id or account_id == def_pay_account_id:
+ account_id = (account_info.default_credit_account_id and
+ account_info.default_credit_account_id.id)
+ else:
+ if len(partner_banks) == 1:
+ account_id = (
+ partner_banks[0].partner_id.property_account_receivable and
+ partner_banks[0].partner_id.property_account_receivable.id)
+ if len(partner_banks) != 1 or not account_id or account_id == def_rec_account_id:
+ account_id = (account_info.default_debit_account_id and
+ account_info.default_debit_account_id.id)
+ values = {}
+ self_values = {}
+ if move_info:
+ results['trans_matched_cnt'] += 1
+ self_values['match_type'] = move_info['match_type']
+ self_values['payment_line_id'] = move_info.get('payment_line_id', False)
+ self_values['move_line_ids'] = [(6, 0, move_info.get('move_line_ids') or [])]
+ self_values['invoice_ids'] = [(6, 0, move_info.get('invoice_ids') or [])]
+ self_values['payment_order_ids'] = [(6, 0, move_info.get('payment_order_ids') or [])]
+ self_values['payment_order_id'] = (move_info.get('payment_order_ids', False) and
+ len(move_info['payment_order_ids']) == 1 and
+ move_info['payment_order_ids'][0]
+ )
+ self_values['move_line_id'] = (move_info.get('move_line_ids', False) and
+ len(move_info['move_line_ids']) == 1 and
+ move_info['move_line_ids'][0]
+ )
+ if move_info['match_type'] == 'invoice':
+ self_values['invoice_id'] = (move_info.get('invoice_ids', False) and
+ len(move_info['invoice_ids']) == 1 and
+ move_info['invoice_ids'][0]
+ )
+ values['partner_id'] = move_info['partner_id']
+ values['partner_bank_id'] = move_info['partner_bank_id']
+ values['type'] = move_info['type']
+ # values['match_type'] = move_info['match_type']
+ else:
+ values['partner_id'] = values['partner_bank_id'] = False
+ if not values['partner_id'] and partner_ids and len(partner_ids) == 1:
+ values['partner_id'] = partner_ids[0]
+ if (not values['partner_bank_id'] and partner_banks and
+ len(partner_banks) == 1):
+ values['partner_bank_id'] = partner_banks[0].id
+ if not transaction.statement_line_id:
+ values.update(dict(
+ name = '%s.%s' % (transaction.statement, transaction.transaction),
+ date = transaction.effective_date,
+ amount = transaction.transferred_amount,
+ statement_id = transaction.statement_id.id,
+ note = transaction.message,
+ ref = transaction.reference,
+ period_id = period_id,
+ currency = account_info.currency_id.id,
+ account_id = account_id,
+ import_transaction_id = transaction.id,
+ ))
+ statement_line_id = statement_line_obj.create(cr, uid, values, context)
+ results['trans_loaded_cnt'] += 1
+ self_values['statement_line_id'] = statement_line_id
+ if transaction.statement_id.id not in imported_statement_ids:
+ imported_statement_ids.append(transaction.statement_id.id)
+ else:
+ statement_line_obj.write(
+ cr, uid, transaction.statement_line_id.id, values, context)
+ self.write(cr, uid, transaction.id, self_values, context)
+ if not injected:
+ i += 1
+
+ #recompute statement end_balance for validation
+ if imported_statement_ids:
+ statement_obj.button_dummy(
+ cr, uid, imported_statement_ids, context=context)
+
+ if payment_lines:
+ # As payments lines are treated as individual transactions, the
+ # batch as a whole is only marked as 'done' when all payment lines
+ # have been reconciled.
+ cr.execute(
+ "SELECT DISTINCT o.id "
+ "FROM payment_order o, payment_line l "
+ "WHERE o.state = 'sent' "
+ "AND o.id = l.order_id "
+ "AND o.id NOT IN ("
+ "SELECT DISTINCT order_id AS id "
+ "FROM payment_line "
+ "WHERE date_done IS NULL "
+ "AND id IN (%s)"
+ ")" % (','.join([str(x) for x in payment_line_ids]))
+ )
+ order_ids = [x[0] for x in cr.fetchall()]
+ if order_ids:
+ # Use workflow logics for the orders. Recode logic from
+ # account_payment, in order to increase efficiency.
+ payment_order_obj.set_done(cr, uid, order_ids,
+ {'state': 'done'}
+ )
+ wf_service = netsvc.LocalService('workflow')
+ for id in order_ids:
+ wf_service.trg_validate(
+ uid, 'payment.order', id, 'done', cr)
+
+ def _get_residual(self, cr, uid, ids, name, args, context=None):
+ """
+ Calculate the residual against the candidate reconciliation.
+ When
+
+ 55 debiteuren, 50 binnen: amount > 0, residual > 0
+ -55 crediteuren, -50 binnen: amount = -60 residual -55 - -50
+
+ - residual > 0 and transferred amount > 0, or
+ - residual < 0 and transferred amount < 0
+
+ the result is a partial reconciliation. In the other cases,
+ a new statement line can be split off.
+
+ We should give users the option to reconcile with writeoff
+ or partial reconciliation / new statement line
+ """
+
+ if not ids:
+ return {}
+ res = dict([(x, False) for x in ids])
+ move_line_obj = self.pool.get('account.move.line')
+ for transaction in self.browse(cr, uid, ids, context):
+ if (transaction.statement_line_id.state == 'draft'
+ and transaction.match_type in
+ [('invoice'), ('move'), ('manual')]
+ and transaction.move_line_id):
+ rec_moves = (
+ transaction.move_line_id.reconcile_id and
+ transaction.move_line_id.reconcile_id.line_id or
+ transaction.move_line_id.reconcile_partial_id and
+ transaction.move_line_id.reconcile_partial_id.line_partial_ids or
+ [transaction.move_line_id])
+ res[transaction.id] = (
+ move_line_obj.get_balance(cr, uid, [x.id for x in rec_moves])
+ - transaction.transferred_amount)
+ return res
+
+ def _get_match_multi(self, cr, uid, ids, name, args, context=None):
+ """
+ Indicate in the wizard that multiple matches have been found
+ and that the user has not yet made a choice between them.
+ """
+ if not ids:
+ return {}
+ res = dict([(x, False) for x in ids])
+ for transaction in self.browse(cr, uid, ids, context):
+ if transaction.match_type == 'move':
+ if transaction.move_line_ids and not transaction.move_line_id:
+ res[transaction.id] = True
+ elif transaction.match_type == 'invoice':
+ if transaction.invoice_ids and not transaction.invoice_id:
+ res[transaction.id] = True
+ elif transaction.match_type == 'payment_order':
+ if (transaction.payment_order_ids and not
+ transaction.payment_order_id):
+ res[transaction.id] = True
+ return res
+
+ def clear_and_write(self, cr, uid, ids, vals=None, context=None):
+ """
+ Write values in argument 'vals', but clear all match
+ related values first
+ """
+ write_vals = (dict([(x, False) for x in [
+ 'match_type',
+ 'move_line_id',
+ 'invoice_id',
+ 'manual_invoice_id',
+ 'manual_move_line_id',
+ 'payment_line_id',
+ ]] +
+ [(x, [(6, 0, [])]) for x in [
+ 'move_line_ids',
+ 'invoice_ids',
+ 'payment_order_ids',
+ ]]))
+ write_vals.update(vals or {})
+ return self.write(cr, uid, ids, write_vals, context=context)
+
+ column_map = {
+ # used in bank_import.py, converting non-osv transactions
+ 'statement_id': 'statement',
+ 'id': 'transaction'
+ }
+
+ _columns = {
+ # start mem_bank_transaction atributes
+ # see parsers/models.py
+ 'transaction': fields.char('transaction', size=16), # id
+ 'statement': fields.char('statement', size=16), # statement_id
+ 'type': fields.char('type', size=16),
+ 'reference': fields.char('reference', size=1024),
+ 'local_account': fields.char('local_account', size=24),
+ 'local_currency': fields.char('local_currency', size=16),
+ 'execution_date': fields.date('execution_date'),
+ 'effective_date': fields.date('effective_date'),
+ 'remote_account': fields.char('remote_account', size=24),
+ 'remote_currency': fields.char('remote_currency', size=16),
+ 'exchange_rate': fields.float('exchange_rate'),
+ 'transferred_amount': fields.float('transferred_amount'),
+ 'message': fields.char('message', size=1024),
+ 'remote_owner': fields.char('remote_owner', size=24),
+ 'remote_owner_address': fields.char('remote_owner_address', size=24),
+ 'remote_owner_city': fields.char('remote_owner_city', size=24),
+ 'remote_owner_postalcode': fields.char('remote_owner_postalcode', size=24),
+ 'remote_owner_country_code': fields.char('remote_owner_country_code', size=24),
+ 'remote_owner_custno': fields.char('remote_owner_custno', size=24),
+ 'remote_bank_bic': fields.char('remote_bank_bic', size=24),
+ 'remote_bank_bei': fields.char('remote_bank_bei', size=24),
+ 'remote_bank_ibei': fields.char('remote_bank_ibei', size=24),
+ 'remote_bank_eangl': fields.char('remote_bank_eangln', size=24),
+ 'remote_bank_chips_uid': fields.char('remote_bank_chips_uid', size=24),
+ 'remote_bank_duns': fields.char('remote_bank_duns', size=24),
+ 'remote_bank_tax_id': fields.char('remote_bank_tax_id', size=24),
+ 'provision_costs': fields.float('provision_costs', size=24),
+ 'provision_costs_currency': fields.char('provision_costs_currency', size=64),
+ 'provision_costs_description': fields.char('provision_costs_description', size=24),
+ 'error_message': fields.char('error_message', size=1024),
+ 'storno_retry': fields.boolean('storno_retry'),
+ # end of mem_bank_transaction_fields
+ 'bank_country_code': fields.char(
+ 'Bank country code', size=2,
+ help=("Fallback default country for new partner records, "
+ "as defined by the import parser"),
+ readonly=True,),
+ 'company_id': fields.many2one(
+ 'res.company', 'Company', required=True),
+ 'duplicate': fields.boolean('duplicate'),
+ 'statement_line_id': fields.many2one(
+ 'account.bank.statement.line', 'Statement line',
+ ondelete='CASCADE'),
+ 'statement_id': fields.many2one(
+ 'account.bank.statement', 'Statement'),
+ 'parent_id': fields.many2one(
+ 'banking.import.transaction', 'Split off from this transaction'),
+ # match fields
+ 'match_type': fields.selection(
+ [('manual', 'Manual'), ('move','Move'), ('invoice', 'Invoice'),
+ ('payment', 'Payment'), ('payment_order', 'Payment order'),
+ ('storno', 'Storno')],
+ 'Match type'),
+ 'match_multi': fields.function(
+ _get_match_multi, method=True, string='Multi match',
+ type='boolean'),
+ 'payment_order_ids': fields.many2many(
+ 'payment.order', 'banking_transaction_payment_order_rel',
+ 'order_id', 'transaction_id', 'Payment orders'),
+ 'payment_order_id': fields.many2one(
+ 'payment.order', 'Payment order to reconcile'),
+ 'move_line_ids': fields.many2many(
+ 'account.move.line', 'banking_transaction_move_line_rel',
+ 'move_line_id', 'transaction_id', 'Matching entries'),
+ 'move_line_id': fields.many2one(
+ 'account.move.line', 'Entry to reconcile'),
+ 'payment_line_id': fields.many2one('payment.line', 'Payment line'),
+ 'invoice_ids': fields.many2many(
+ 'account.invoice', 'banking_transaction_invoice_rel',
+ 'invoice_id', 'transaction_id', 'Matching invoices'),
+ 'invoice_id': fields.many2one(
+ 'account.invoice', 'Invoice to reconcile'),
+ 'payment_line_id': fields.many2one('payment.line', 'Payment line'),
+ 'residual': fields.function(
+ _get_residual, method=True, string='Residual', type='float'),
+ 'writeoff_account_id': fields.many2one(
+ 'account.account', 'Write-off account',
+ domain=[('type', '!=', 'view')]),
+ 'writeoff_journal_id': fields.many2one(
+ 'account.journal', 'Write-off journal'),
+ 'writeoff_move_line_id': fields.many2one(
+ 'account.move.line', 'Write off move line'),
+ }
+ _defaults = {
+ 'company_id': lambda s,cr,uid,c:
+ s.pool.get('res.company')._company_default_get(
+ cr, uid, 'bank.import.transaction', context=c),
+ }
+banking_import_transaction()
+
+class account_bank_statement_line(osv.osv):
+ _inherit = 'account.bank.statement.line'
+ _columns = {
+ 'import_transaction_id': fields.many2one(
+ 'banking.import.transaction',
+ 'Import transaction', readonly=True),
+ 'match_multi': fields.related(
+ 'import_transaction_id', 'match_multi', type='boolean',
+ string='Multi match', readonly=True),
+ 'residual': fields.related(
+ 'import_transaction_id', 'residual', type='float',
+ string='Residual'),
+ 'duplicate': fields.related(
+ 'import_transaction_id', 'duplicate', type='boolean',
+ string='Possible duplicate import', readonly=True),
+ 'match_type': fields.related(
+ 'import_transaction_id', 'match_type', type='selection',
+ selection=[('manual', 'Manual'), ('move','Move'),
+ ('invoice', 'Invoice'), ('payment', 'Payment'),
+ ('payment_order', 'Payment order'),
+ ('storno', 'Storno')],
+ string='Match type', readonly=True,),
+ 'residual': fields.related(
+ 'import_transaction_id', 'residual', type='float',
+ string='Residual', readonly=True,
+ ),
+ 'state': fields.selection(
+ [('draft', 'Draft'), ('confirmed', 'Confirmed')], 'State',
+ readonly=True, required=True),
+ 'move_id': fields.many2one(
+ 'account.move', 'Move', readonly=True,
+ help="The accounting move associated with this line"),
+ }
+
+ _defaults = {
+ 'state': 'draft',
+ }
+
+ def match_wizard(self, cr, uid, ids, context=None):
+ res = False
+ if ids:
+ if isinstance(ids, (int, float)):
+ ids = [ids]
+ if context is None:
+ context = {}
+ context['statement_line_id'] = ids[0]
+ wizard_obj = self.pool.get('banking.transaction.wizard')
+ res_id = wizard_obj.create(
+ cr, uid, {'statement_line_id': ids[0]}, context=context)
+ res = wizard_obj.create_act_window(cr, uid, res_id, context=context)
+ return res
+
+ def confirm(self, cr, uid, ids, context=None):
+ # TODO: a confirmed transaction should remove its reconciliation target
+ # from other transactions where it is one of multiple candidates or
+ # even the proposed reconciliation target.
+ statement_obj = self.pool.get('account.bank.statement')
+ obj_seq = self.pool.get('ir.sequence')
+ import_transaction_obj = self.pool.get('banking.import.transaction')
+
+ for st_line in self.browse(cr, uid, ids, context):
+ if st_line.state != 'draft':
+ continue
+ if st_line.duplicate:
+ raise osv.except_osv(
+ _('Bank transfer flagged as duplicate'),
+ _("You cannot confirm a bank transfer marked as a "
+ "duplicate (%s.%s)") %
+ (st_line.statement_id.name, st_line.name,))
+ if st_line.analytic_account_id:
+ if not st_line.statement_id.journal_id.analytic_journal_id:
+ raise osv.except_osv(
+ _('No Analytic Journal !'),
+ _("You have to define an analytic journal on the '%s' "
+ "journal!") % (st_line.statement_id.journal_id.name,))
+ if not st_line.amount:
+ continue
+ if st_line.import_transaction_id:
+ import_transaction_obj.reconcile(
+ cr, uid, st_line.import_transaction_id.id, context)
+
+ if not st_line.statement_id.name == '/':
+ st_number = st_line.statement_id.name
+ else:
+ if st_line.statement_id.journal_id.sequence_id:
+ c = {'fiscalyear_id':
+ st_line.statement_id.period_id.fiscalyear_id.id}
+ st_number = obj_seq.get_id(
+ cr, uid,
+ st_line.statement_id.journal_id.sequence_id.id,
+ context=c)
+ else:
+ st_number = obj_seq.get(cr, uid, 'account.bank.statement')
+ statement_obj.write(
+ cr, uid, [st_line.statement_id.id],
+ {'name': st_number}, context=context)
+
+ st_line_number = statement_obj.get_next_st_line_number(
+ cr, uid, st_number, st_line, context)
+ company_currency_id = st_line.statement_id.journal_id.company_id.currency_id.id
+ move_id = statement_obj.create_move_from_st_line(
+ cr, uid, st_line.id, company_currency_id,
+ st_line_number, context)
+ self.write(
+ cr, uid, st_line.id,
+ {'state': 'confirmed', 'move_id': move_id}, context)
+ return True
+
+ def cancel(self, cr, uid, ids, context=None):
+ if ids and isinstance(ids, (int, float)):
+ ids = [ids]
+ account_move_obj = self.pool.get('account.move')
+ import_transaction_obj = self.pool.get('banking.import.transaction')
+ transaction_cancel_ids = []
+ move_unlink_ids = []
+ set_draft_ids = []
+ # harvest ids for various actions
+ for st_line in self.browse(cr, uid, ids, context):
+ if st_line.state != 'confirmed':
+ continue
+ if st_line.statement_id.state != 'draft':
+ raise osv.except_osv(
+ _("Cannot cancel bank transaction"),
+ _("The bank statement that this transaction belongs to has "
+ "already been confirmed"))
+ if st_line.import_transaction_id:
+ transaction_cancel_ids.append(st_line.import_transaction_id.id)
+ if st_line.move_id:
+ move_unlink_ids.append(st_line.move_id.id)
+ else:
+ raise osv.except_osv(
+ _("Cannot cancel bank transaction"),
+ _("Cannot cancel this bank transaction. The information "
+ "needed to undo the accounting entries has not been "
+ "recorded"))
+ set_draft_ids.append(st_line.id)
+ # perform actions
+ import_transaction_obj.cancel(
+ cr, uid, transaction_cancel_ids, context=context)
+ account_move_obj.button_cancel(cr, uid, move_unlink_ids, context)
+ account_move_obj.unlink(cr, uid, move_unlink_ids, context)
+ self.write(
+ cr, uid, set_draft_ids, {'state': 'draft'}, context=context)
+ return True
+account_bank_statement_line()
+
+class account_bank_statement(osv.osv):
+ _inherit = 'account.bank.statement'
+
+ def _end_balance(self, cursor, user, ids, name, attr, context=None):
+ """
+ This method taken from account/account_bank_statement.py and
+ altered to take the statement line subflow into account
+ """
+
+ res_currency_obj = self.pool.get('res.currency')
+ res_users_obj = self.pool.get('res.users')
+ res = {}
+
+ company_currency_id = res_users_obj.browse(cursor, user, user,
+ context=context).company_id.currency_id.id
+
+ statements = self.browse(cursor, user, ids, context=context)
+ for statement in statements:
+ res[statement.id] = statement.balance_start
+ currency_id = statement.currency.id
+ for line in statement.move_line_ids:
+ if line.debit > 0:
+ if line.account_id.id == \
+ statement.journal_id.default_debit_account_id.id:
+ res[statement.id] += res_currency_obj.compute(cursor,
+ user, company_currency_id, currency_id,
+ line.debit, context=context)
+ else:
+ if line.account_id.id == \
+ statement.journal_id.default_credit_account_id.id:
+ res[statement.id] -= res_currency_obj.compute(cursor,
+ user, company_currency_id, currency_id,
+ line.credit, context=context)
+ if statement.state == 'draft':
+ for line in statement.line_ids:
+ ### start modifications banking-addons ###
+ # res[statement.id] += line.amount
+ if line.state == 'draft':
+ res[statement.id] += line.amount
+ ### end modifications banking-addons ###
+
+ for r in res:
+ res[r] = round(res[r], 2)
+ return res
+
+ def button_confirm_bank(self, cr, uid, ids, context=None):
+ """ Inject the statement line workflow here """
+ if context is None:
+ context = {}
+ line_obj = self.pool.get('account.bank.statement.line')
+ for st in self.browse(cr, uid, ids, context=context):
+ j_type = st.journal_id.type
+ if not self.check_status_condition(cr, uid, st.state, journal_type=j_type):
+ continue
+
+ self.balance_check(cr, uid, st.id, journal_type=j_type, context=context)
+ if (not st.journal_id.default_credit_account_id) \
+ or (not st.journal_id.default_debit_account_id):
+ raise osv.except_osv(_('Configuration Error !'),
+ _('Please verify that an account is defined in the journal.'))
+
+ # protect against misguided manual changes
+ for line in st.move_line_ids:
+ if line.state <> 'valid':
+ raise osv.except_osv(_('Error !'),
+ _('The account entries lines are not in valid state.'))
+
+ line_obj.confirm(cr, uid, [line.id for line in st.line_ids], context)
+ self.log(cr, uid, st.id, _('Statement %s is confirmed, journal '
+ 'items are created.') % (st.name,))
+ return self.write(cr, uid, ids, {'state':'confirm'}, context=context)
+
+ def button_cancel(self, cr, uid, ids, context=None):
+ """
+ Do nothing but write the state. Delegate all actions to the statement
+ line workflow instead.
+ """
+ self.write(cr, uid, ids, {'state':'draft'}, context=context)
+
+ _columns = {
+ # override this field *only* to replace the
+ # function method with the one from this module.
+ # Note that it is defined twice, both in
+ # account/account_bank_statement.py (without 'store') and
+ # account/account_cash_statement.py (with store=True)
+
+ 'balance_end': fields.function(
+ _end_balance, method=True, store=True, string='Balance'),
+ }
+
+account_bank_statement()
+>>>>>>> MERGE-SOURCE
=== modified file 'account_banking/wizard/bank_import.py'
--- account_banking/wizard/bank_import.py 2012-02-20 19:41:49 +0000
+++ account_banking/wizard/bank_import.py 2012-02-21 12:27:17 +0000
@@ -117,8 +117,12 @@
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')
+<<<<<<< TREE
import_transaction_obj = self.pool.get('banking.import.transaction')
period_obj = self.pool.get('account.period')
+=======
+ import_transaction_obj = self.pool.get('banking.import.transaction')
+>>>>>>> MERGE-SOURCE
# get the parser to parse the file
parser_code = banking_import.parser
@@ -279,6 +283,7 @@
imported_statement_ids.append(statement_id)
subno = 0
+<<<<<<< TREE
for transaction in statement.transactions:
subno += 1
if not transaction.id:
@@ -300,8 +305,29 @@
else:
osv.except_osv('Failed to create an import transaction resource','')
+=======
+ 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
+ transaction_id = import_transaction_obj.create(cursor, uid, values, context=context)
+ if transaction_id:
+ transaction_ids.append(transaction_id)
+ else:
+ osv.except_osv('Failed to create an import transaction resource','')
+
+>>>>>>> MERGE-SOURCE
results.stat_loaded_cnt += 1
+<<<<<<< TREE
import_transaction_obj.match(cursor, uid, transaction_ids, results=results, context=context)
#recompute statement end_balance for validation
@@ -328,6 +354,10 @@
# )
#)
+=======
+ import_transaction_obj.match(cursor, uid, transaction_ids, results=results, context=context)
+
+>>>>>>> MERGE-SOURCE
report = [
'%s: %s' % (_('Total number of statements'),
results.stat_skipped_cnt + results.stat_loaded_cnt),
=== modified file 'account_banking/wizard/bank_import_view.xml'
--- account_banking/wizard/bank_import_view.xml 2012-02-17 23:36:43 +0000
+++ account_banking/wizard/bank_import_view.xml 2012-02-21 12:27:17 +0000
@@ -15,6 +15,7 @@
<field name="parser"/>
<field name="state" invisible="1"/>
</group>
+<<<<<<< TREE
<notebook colspan="4">
<page attrs="{'invisible': [('state', '!=', 'review')]}" string="Transactions">
<field name="line_ids" colspan="4" nolabel="1">
@@ -38,6 +39,31 @@
</page>
</notebook>
<group colspan="2" >
+=======
+ <notebook colspan="4">
+ <page attrs="{'invisible': [('state', '!=', 'review')]}" string="Transactions">
+ <field name="line_ids" colspan="4" nolabel="1">
+ <tree string="Transaction" min_rows="20" 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"/>
+ </page>
+ </notebook>
+ <group colspan="2" >
+>>>>>>> MERGE-SOURCE
<button icon="gtk-cancel"
special="cancel"
states="init"
=== modified file 'account_banking/wizard/banking_transaction_wizard.py'
--- account_banking/wizard/banking_transaction_wizard.py 2012-02-20 19:41:49 +0000
+++ account_banking/wizard/banking_transaction_wizard.py 2012-02-21 12:27:17 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
# -*- encoding: utf-8 -*-
##############################################################################
#
@@ -343,3 +344,342 @@
}
banking_transaction_wizard()
+=======
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
+# (C) 2011 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 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 osv import osv, fields
+
+"""
+
+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(osv.osv_memory):
+ _name = 'banking.transaction.wizard'
+ _description = 'Match transaction'
+
+ def create_act_window(self, cr, uid, ids, nodestroy=True, context=None):
+ """
+ Return a popup window for this model
+ """
+ if isinstance(ids, (int, float)):
+ 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, float)):
+ 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 True
+
+ 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:
+ return True
+
+ # The following fields get never written
+ # they are just triggers for manual matching
+ # which populates regular fields on the transaction
+ manual_invoice_id = vals.pop('manual_invoice_id', False)
+ manual_move_line_id = vals.pop('manual_move_line_id', False)
+
+ # Support for writing fields.related is still flakey:
+ # https://bugs.launchpad.net/openobject-server/+bug/915975
+ # Will do so myself.
+
+ # Separate the related fields
+ transaction_vals = {}
+ wizard_vals = vals.copy()
+ for key in vals.keys():
+ field = self._columns[key]
+ if (isinstance(field, fields.related) and
+ field._arg[0] == 'import_transaction_id'):
+ transaction_vals[field._arg[1]] = vals[key]
+ del wizard_vals[key]
+
+ # write the related fields on the transaction model
+ for wizard in self.read(
+ cr, uid, ids, ['import_transaction_id'], context=context):
+ if wizard['import_transaction_id']:
+ transaction_obj.write(
+ cr, uid, wizard['import_transaction_id'][0],
+ transaction_vals, context=context)
+
+ # write other fields to the wizard model
+ res = super(banking_transaction_wizard, self).write(
+ cr, uid, ids, wizard_vals, context=context)
+
+ # End of workaround for lp:915975
+
+ """ Process the logic of the written values """
+
+ # An invoice is selected from multiple candidates
+ if vals and 'invoice_id' in vals:
+ for wiz in self.browse(cr, uid, ids, context=context):
+ if (wiz.import_transaction_id.match_type == 'invoice' and
+ wiz.import_transaction_id.invoice_id):
+ # the current value might apply
+ if (wiz.move_line_id and wiz.move_line_id.invoice and
+ wiz.move_line_id.invoice.id == wiz.invoice_id.id):
+ found = True
+ continue
+ # 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.id ==
+ wiz.import_transaction_id.invoice_id.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.invoice.partner_id.id,
+ 'account_id': move_line.account_id.id,
+ }, context=context)
+ found = True
+ break
+ # Cannot match the invoice
+ if not found:
+ # transaction_obj.write(
+ # cr, uid, wiz.import_transaction_id.id,
+ # { 'invoice_id': False, }, context=context)
+ osv.except_osv(
+ _("No entry found for the selected invoice"),
+ _("No entry found for the selected invoice. " +
+ "Try manual reconciliation."))
+
+ if manual_move_line_id or manual_invoice_id:
+ 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')
+ for wiz in self.browse(
+ cr, uid, ids, context=context):
+ invoice_ids = False
+ move_line_id = False
+ move_line_ids = False
+ invoice_id = manual_invoice_id
+ if invoice_id:
+ invoice = invoice_obj.browse(
+ cr, uid, manual_invoice_id, context=context)
+ if invoice.move_id:
+ for line in invoice.move_id.line_id:
+ if line.account_id.type in ('receivable', 'payable'):
+ move_line_id = line.id
+ break
+ if not move_line_id:
+ osv.except_osv(
+ _("Cannot select for reconcilion"),
+ _("No entry found for the selected invoice. "))
+ else:
+ move_line_id = manual_move_line_id
+ move_line = move_line_obj.read(
+ cr, uid, move_line_id, ['invoice'], context=context)
+ invoice_id = (move_line['invoice'] and
+ move_line['invoice'][0])
+ vals = {
+ 'move_line_id': move_line_id,
+ 'move_line_ids': [(6, 0, [move_line_id])],
+ 'invoice_id': invoice_id,
+ 'invoice_ids': [(6, 0, invoice_id and
+ [invoice_id] or [])],
+ 'match_type': 'manual',
+ }
+ transaction_obj.clear_and_write(
+ cr, uid, wiz.import_transaction_id.id,
+ vals, context=context)
+ st_line_vals = {
+ 'account_id': move_line_obj.read(
+ cr, uid, move_line_id,
+ ['account_id'], context=context)['account_id'][0],
+ }
+ if invoice_id:
+ st_line_vals['partner_id'] = invoice_obj.read(
+ cr, uid, invoice_id,
+ ['partner_id'], context=context)['partner_id'][0]
+ statement_line_obj.write(
+ cr, uid, wiz.import_transaction_id.statement_line_id.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 True
+
+ def disable_match(self, cr, uid, ids, context=None):
+ """
+ Clear manual and automatic match information
+ """
+ if isinstance(ids, (int, float)):
+ ids = [ids]
+# self.write(cr, uid, ids,
+# {'manual_invoice_id': False,
+# 'manual_move_line_id': False,
+# }, context=context)
+ wizs = self.read(
+ cr, uid, ids, ['import_transaction_id'], context=context)
+ trans_ids = [x['import_transaction_id'][0] for x in wizs
+ if x['import_transaction_id']]
+ return self.pool.get('banking.import.transaction').clear_and_write(
+ cr, uid, trans_ids, context=context)
+
+ def reverse_duplicate(self, cr, uid, ids, context=None):
+ if isinstance(ids, (int, float)):
+ 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 True
+
+ def _get_default_match_type(self, cr, uid, context=None):
+ """
+ Take initial value for the match type from the statement line
+ """
+ res = False
+ if context and 'statement_line_id' in context:
+ res = self.pool.get('account.bank.statement.line').read(
+ cr, uid, context['statement_line_id'],
+ ['match_type'], context=context)['match_type']
+ return res
+
+ def button_done(self, cr, uid, ids, context=None):
+ return {'nodestroy': False, 'type': 'ir.actions.act_window_close'}
+
+ _defaults = {
+# 'match_type': _get_default_match_type,
+ }
+
+ _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),
+ 'partner_id': fields.related(
+ 'statement_line_id', 'partner_id',
+ type='many2one', relation='res.partner',
+ string="Partner", 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'),
+ 'writeoff_journal_id': fields.related(
+ 'import_transaction_id', 'writeoff_journal_id',
+ type='many2one', relation='account.journal',
+ string='Write-off journal'),
+ 'payment_line_id': fields.related(
+ 'import_transaction_id', 'payment_line_id', string="Matching payment or storno",
+ type='many2one', relation='payment.line', readonly=True),
+ 'payment_order_ids': fields.related(
+ 'import_transaction_id', 'payment_order_ids', string="Matching payment orders",
+ type='many2many', relation='payment.order'),
+ 'payment_order_id': fields.related(
+ 'import_transaction_id', 'payment_order_id', string="Payment order to reconcile",
+ type='many2one', relation='payment.order'),
+ '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="char", size=16, string='Match type', readonly=True),
+ 'manual_invoice_id': fields.many2one(
+ 'account.invoice', 'Match this invoice',
+ domain=[('reconciled', '=', False)]),
+ 'manual_move_line_id': fields.many2one(
+ 'account.move.line', 'Or match this entry',
+ domain=[('account_id.reconcile', '=', True),
+ ('reconcile_id', '=', False)],
+ #'manual_payment_order_id': fields.many2one(
+ # 'payment.order', "Payment order to reconcile"),
+ ),
+ }
+banking_transaction_wizard()
+
+>>>>>>> MERGE-SOURCE
=== modified file 'account_banking/wizard/banking_transaction_wizard.xml'
--- account_banking/wizard/banking_transaction_wizard.xml 2012-02-20 19:41:49 +0000
+++ account_banking/wizard/banking_transaction_wizard.xml 2012-02-21 12:27:17 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
@@ -133,3 +134,138 @@
</record>
</data>
</openerp>
+=======
+<?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="type">form</field>
+ <field name="model">banking.transaction.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Match transaction">
+ <!-- fields used for form logic -->
+ <field name="payment_order_ids" 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"/>
+ <field name="date"/>
+ <field name="amount"/>
+ <field name="ref"/>
+ <field name="partner_id"/>
+ </group>
+
+ <!-- (semi-) automatic matching and selection -->
+
+ <group colspan="2" col="4">
+
+ <separator string="Current match" colspan="4"/>
+ <field name="match_type"/>
+ <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>
+ <field name='payment_line_id'
+ attrs="{'invisible': [('match_type', '!=', 'storno'),('match_type', '!=', 'payment')]}"
+ />
+ <group attrs="{'readonly': [('match_multi', '!=' True)]}">
+ <!-- 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)]}"
+ 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)]}"
+ domain="[('id', 'in', move_line_ids[0][2])]"
+ />
+ <field name='payment_order_id'
+ attrs="{'readonly': [('match_multi', '=', False)], 'invisible': [('match_type', '!=', 'payment_order')]}"
+ domain="[('id', 'in', payment_order_ids[0][2])]"
+ />
+ </group>
+ <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>
+ <page string="Write-Off" attrs="{'invisible': [('match_type', '=', False)]}">
+ <group colspan="2" col="2">
+ <label string="If the amount exceeds the match, you must set a write-off account and journal for the residual of this reconciliation. If the amount is smaller than the match, this is optional. If you do not set a write-off account in this case, the result will be a partial reconciliation." colspan="2"/>
+ <!-- no luck with these lt/gt calculations in attrs -->
+ <field name="writeoff_account_id"
+ attrs="{'required': ['|','&',('residual', '<', 0),('amount', '>', 0),'&',('residual', '>', 0),('amount', '<', 0)]}"/>
+ <field name="writeoff_journal_id"
+ attrs="{'required': ['|','&',('residual', '<', 0),('amount', '>', 0),'&',('residual', '>', 0),('amount', '<', 0)]}"/>
+ <button colspan="1"
+ name="trigger_write"
+ type="object"
+ string="Set write-off account"/>
+ </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"/>
+ <!-- Manual selection -->
+ </page>
+ <page string="Manual match">
+ <field name="manual_invoice_id"/>
+ <field name="manual_move_line_id"/>
+ <newline/>
+ <button colspan="1"
+ name="trigger_write"
+ type="object"
+ string="Match"/>
+ </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>
+ <group colspan="2">
+ <separator/>
+ <button icon="gtk-ok" string="Done" special="cancel"/>
+ </group>
+ </group>
+ </form>
+ </field>
+ </record>
+ </data>
+</openerp>
+>>>>>>> MERGE-SOURCE
=== modified file 'account_banking/wizard/banktools.py'
=== added directory 'account_iban_preserve_domestic'
=== renamed directory 'account_iban_preserve_domestic' => 'account_iban_preserve_domestic.moved'
=== added file 'account_iban_preserve_domestic/__init__.py'
--- account_iban_preserve_domestic/__init__.py 1970-01-01 00:00:00 +0000
+++ account_iban_preserve_domestic/__init__.py 2012-02-21 12:27:17 +0000
@@ -0,0 +1,1 @@
+import res_partner_bank
=== added file 'account_iban_preserve_domestic/__openerp__.py'
--- account_iban_preserve_domestic/__openerp__.py 1970-01-01 00:00:00 +0000
+++ account_iban_preserve_domestic/__openerp__.py 2012-02-21 12:27:17 +0000
@@ -0,0 +1,57 @@
+##############################################################################
+#
+# Copyright (C) 2012 Therp BV (<http://therp.nl>).
+#
+# All other contributions are (C) by their respective contributors
+#
+# All Rights Reserved
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract EduSense BV
+#
+# 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/>.
+#
+##############################################################################
+{
+ 'name': 'Preserve domestic bank account number',
+ 'version': '0.1',
+ 'license': 'GPL-3',
+ 'author': 'Therp BV',
+ 'website': 'https://launchpad.net/banking-addons',
+ 'category': 'Banking addons',
+ 'depends': ['base_iban'],
+ 'init_xml': [],
+ 'update_xml': [],
+ 'demo_xml': [],
+ 'description': '''
+This module is compatible with OpenERP 6.0.
+
+The IBAN module in OpenERP 6.1 registers the IBAN
+on the same field as the domestic account number,
+instead of keeping both on separate fields as is the
+case in 6.0. That means that an upgrade to OpenERP 6.1
+makes you lose this information. If you want to keep
+the domestic account number in addition to the IBAN,
+install this module prior to the upgrade to OpenERP 6.1.
+
+Do *not* install this version of the module on OpenERP 6.1.
+A dedicated module for OpenERP 6.1 will be available that
+allows you to access the domestic account number.
+ ''',
+ 'active': False,
+ 'installable': True,
+}
=== added file 'account_iban_preserve_domestic/res_partner_bank.py'
--- account_iban_preserve_domestic/res_partner_bank.py 1970-01-01 00:00:00 +0000
+++ account_iban_preserve_domestic/res_partner_bank.py 2012-02-21 12:27:17 +0000
@@ -0,0 +1,29 @@
+from osv import fields,osv
+class res_partner_bank(osv.osv):
+ '''Bank Accounts'''
+ _inherit = "res.partner.bank"
+
+ def _get_domestic(self, cr, uid, ids, prop, unknow_none, context=None):
+ import pdb
+ pdb.set_trace()
+ res = dict(
+ [(x['id'], x['acc_number'])
+ for x in self.read(cr, uid, ids, ['acc_number'], context=context)
+ ]
+ )
+ return res
+
+ _columns = {
+ 'acc_number_domestic': fields.function(
+ _get_domestic, method=True, type="char",
+ size=64, string='Domestic Account Number',
+ store = {
+ 'res.partner.bank':(
+ lambda self,cr,uid,ids,c={}:ids,
+ ['acc_number'], 10),
+ },
+ ),
+ }
+
+res_partner_bank()
+