Improvement around the Transaction IDs modules

==============================================

This proposal aims to improve the modules using transaction ids, I will start
by summarizing what are they used for, then what are the existing problems and
what changes I propose.

Transaction IDs?
----------------

The transaction IDs are a technical reference for a move line. They are to
differentiate from the usual reference that are a reference for humans firstly
(more about that here [0]). Usually, the transaction IDs are defined by
external systems such as payment gateways and are a way to streamline the
reconciliations between the invoices, bank statements...

Changes
-------

1) account_move_line.transaction_ref is defined in 'account_advanced_reconcile_transaction_ref' which adds a reconciliation method with transaction id.
It makes much sense to add the field in 'base_transaction_id' so we can use the field in other modules such as the bank statement completion modules. It is a pity that the field on the invoice and the sale order is 'transaction_id' and in move lines 'transaction_ref' but I prefer to keep the backward-compatibility.

So I moved these things from 'account_advanced_reconcile_transaction_ref' to 'base_transaction_id'

2) In account_advanced_reconcile_transaction_ref there is an inherit of the bank statement that copies the line's ref in the move line's transaction_id. I think this is a mismatch between the ref and the transaction_id that we have to avoid. In fact, only the transaction id of the statement lines should be copied if any, or left empty if the statement line has no transaction id.

3) A consequence of the change 2) is that the automatic reconcile from transaction ref will no longer work for those not using the transaction ids in the bank statement but only the ref. So I added a new reconciliation rule that matches 'ref' vs 'transaction id'. The only drawback is that they will need to change their configuration, but at least the rules will be clear on their intentions.

4) completion rules: 'base_transaction_id' adds a transaction_id on sales order and invoices. There is actually a completion rule that searches the bank statement information from a matching invoice with the same transaction_id. I added the same rule that searches for an invoice with the same transaction id. This is the logical continuation and a good complement when an invoice / refund was not generated by a sales order and we still need to autocomplete the bank statement.

[0] https://code.launchpad.net/~camptocamp/banking-addons/7.0-bank-statement-reconcile-account_invoice_reference/+merge/202689
This commit is contained in:
unknown
2014-04-14 15:09:19 +02:00
committed by unknown
21 changed files with 395 additions and 91 deletions

View File

@@ -18,7 +18,6 @@
#
##############################################################################
from . import account
from . import easy_reconcile
from . import base_advanced_reconciliation
from . import advanced_reconciliation

View File

@@ -20,8 +20,8 @@
{'name': 'Advanced Reconcile Transaction Ref',
'description': """
Advanced reconciliation method for the module account_easy_reconcile
=================================================
Advanced reconciliation method for the module account_advanced_reconcile
========================================================================
Reconcile rules with transaction_ref
""",

View File

@@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Romain Deheele
# Copyright 2013 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv.orm import Model, fields
class AccountMoveLine(Model):
"""
Inherit account.move.line class in order to add transaction_ref field
"""
_inherit = "account.move.line"
_columns = {
'transaction_ref': fields.char('Transaction Ref.', size=128),
}
class AccountBankStatement(Model):
"""
Inherit account.bank.statement class in order to set transaction_ref info on account.move.line
"""
_inherit = "account.bank.statement"
def _prepare_move_line_vals(
self, cr, uid, st_line, move_id, debit, credit, currency_id=False,
amount_currency=False, account_id=False, analytic_id=False,
partner_id=False, context=None):
if context is None:
context = {}
res = super(AccountBankStatement, self)._prepare_move_line_vals(
cr, uid, st_line, move_id, debit, credit,
currency_id=currency_id,
amount_currency=amount_currency,
account_id=account_id,
analytic_id=analytic_id,
partner_id=partner_id, context=context)
res.update({'transaction_ref': st_line.ref})
return res

View File

@@ -32,7 +32,8 @@ class easy_reconcile_advanced_transaction_ref(orm.TransientModel):
will be skipped for reconciliation. Can be inherited to
skip on some conditions. ie: ref or partner_id is empty.
"""
return not (move_line.get('ref') and move_line.get('partner_id'))
return not (move_line.get('transaction_ref') and
move_line.get('partner_id'))
def _matchers(self, cr, uid, rec, move_line, context=None):
return (('partner_id', move_line['partner_id']),
@@ -41,3 +42,25 @@ class easy_reconcile_advanced_transaction_ref(orm.TransientModel):
def _opposite_matchers(self, cr, uid, rec, move_line, context=None):
yield ('partner_id', move_line['partner_id'])
yield ('ref', (move_line['transaction_ref'] or '').lower().strip())
class easy_reconcile_advanced_transaction_ref_vs_ref(orm.TransientModel):
_name = 'easy.reconcile.advanced.trans_ref_vs_ref'
_inherit = 'easy.reconcile.advanced'
def _skip_line(self, cr, uid, rec, move_line, context=None):
"""
When True is returned on some conditions, the credit move line
will be skipped for reconciliation. Can be inherited to
skip on some conditions. ie: ref or partner_id is empty.
"""
return not (move_line.get('ref') and move_line.get('partner_id'))
def _matchers(self, cr, uid, rec, move_line, context=None):
return (('partner_id', move_line['partner_id']),
('ref', move_line['ref'].lower().strip()))
def _opposite_matchers(self, cr, uid, rec, move_line, context=None):
yield ('partner_id', move_line['partner_id'])
yield ('ref', (move_line['transaction_ref'] or '').lower().strip())

View File

@@ -32,6 +32,8 @@ class account_easy_reconcile_method(orm.Model):
methods += [
('easy.reconcile.advanced.transaction_ref',
'Advanced. Partner and Transaction Ref.'),
('easy.reconcile.advanced.trans_ref_vs_ref',
'Advanced. Partner and Transaction Ref. vs Ref.'),
]
return methods

View File

@@ -10,7 +10,12 @@
<group colspan="2" col="2">
<separator colspan="4" string="Advanced. Partner and Transaction Ref"/>
<label string="Match multiple debit vs multiple credit entries. Allow partial reconciliation.
The lines should have the partner, the credit entry transaction ref. is matched vs the debit entry transaction ref. or name." colspan="4"/>
The lines should have the partner, the credit entry transaction ref. is matched vs the debit entry transaction ref." colspan="4"/>
</group>
<group colspan="2" col="2">
<separator colspan="4" string="Advanced. Partner and Transaction Ref. vs Ref."/>
<label string="Match multiple debit vs multiple credit entries. Allow partial reconciliation.
The lines should have the partner, the credit entry reference is matched vs the debit entry transaction reference." colspan="4"/>
</group>
</page>
</field>

View File

@@ -124,7 +124,7 @@ The lines should have the same amount (with the write-off) and the same referenc
<field name="model">account.easy.reconcile.method</field>
<field name="arch" type="xml">
<form string="Automatic Easy Reconcile Method">
<field name="sequence"/>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="write_off"/>
<field name="account_lost_id" attrs="{'required':[('write_off','>',0)]}"/>
@@ -141,7 +141,7 @@ The lines should have the same amount (with the write-off) and the same referenc
<field name="model">account.easy.reconcile.method</field>
<field name="arch" type="xml">
<tree editable="top" string="Automatic Easy Reconcile Method">
<field name="sequence"/>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="write_off"/>
<field name="account_lost_id" attrs="{'required':[('write_off','>',0)]}"/>

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import account_payment

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{'name': 'Account Payment - Transaction ID',
'version': '1.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
'license': 'AGPL-3',
'category': 'Hidden',
'depends': ['base_transaction_id',
'account_payment',
'statement_voucher_killer',
],
'description': """
Compatibility module between Account Payment and Base Transaction ID.
Needs `statement_voucher_killer`
""",
'website': 'http://www.camptocamp.com',
'data': [],
'test': [],
'installable': True,
'auto_install': True,
}

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm
class AccountPaymentPopulateStatement(orm.TransientModel):
_inherit = "account.payment.populate.statement"
def _prepare_statement_line_vals(self, cr, uid, payment_line, amount,
statement, context=None):
superself = super(AccountPaymentPopulateStatement, self)
vals = superself._prepare_statement_line_vals(
cr, uid, payment_line, amount, statement, context=context)
if payment_line.move_line_id:
vals['transaction_id'] = payment_line.move_line_id.transaction_ref
return vals
class account_statement_from_invoice_lines(orm.TransientModel):
_inherit = "account.statement.from.invoice.lines"
def _prepare_statement_line_vals(self, cr, uid, move_line, s_type,
statement_id, amount, context=None):
superself = super(account_statement_from_invoice_lines, self)
vals = superself._prepare_statement_line_vals(
cr, uid, move_line, s_type, statement_id, amount, context=context)
vals['transaction_id'] = move_line.transaction_ref
return vals

View File

@@ -51,6 +51,8 @@
'test': [
'test/sale.yml',
'test/completion_transactionid_test.yml',
'test/invoice.yml',
'test/completion_invoice_transactionid_test.yml',
],
'installable': True,
'images': [],

View File

@@ -3,10 +3,16 @@
<data noupdate="1">
<record id="bank_statement_completion_rule_4" model="account.statement.completion.rule">
<field name="name">Match from line reference (based on transaction ID)</field>
<field name="name">Match from Sales Order using transaction ID</field>
<field name="sequence">30</field>
<field name="function_to_call">get_from_transaction_id_and_so</field>
</record>
<record id="bank_statement_completion_rule_trans_id_invoice" model="account.statement.completion.rule">
<field name="name">Match from Invoice using transaction ID</field>
<field name="sequence">40</field>
<field name="function_to_call">get_from_transaction_id_and_invoice</field>
</record>
</data>
</openerp>

View File

@@ -33,8 +33,12 @@ class AccountStatementCompletionRule(Model):
def _get_functions(self, cr, uid, context=None):
res = super(AccountStatementCompletionRule, self)._get_functions(
cr, uid, context=context)
res.append(('get_from_transaction_id_and_so',
'From line reference (based on SO transaction ID)'))
res += [
('get_from_transaction_id_and_so',
'Match Sales Order using transaction ID'),
('get_from_transaction_id_and_invoice',
'Match Invoice using transaction ID'),
]
return res
_columns = {
@@ -79,6 +83,52 @@ class AccountStatementCompletionRule(Model):
res.update(st_vals)
return res
def get_from_transaction_id_and_invoice(self, cr, uid, st_line, context=None):
"""
Match the partner based on the transaction ID field of the invoice.
Then, call the generic st_line method to complete other values.
In that case, we always fullfill the reference of the line with the invoice name.
:param dict st_line: read of the concerned account.bank.statement.line
:return:
A dict of value that can be passed directly to the write method of
the statement line or {}
{'partner_id': value,
'account_id' : value,
...}
"""
st_obj = self.pool.get('account.bank.statement.line')
res = {}
invoice_obj = self.pool.get('account.invoice')
invoice_id = invoice_obj.search(
cr, uid,
[('transaction_id', '=', st_line['transaction_id'])],
context=context)
if len(invoice_id) > 1:
raise ErrorTooManyPartner(
_('Line named "%s" (Ref:%s) was matched by more than '
'one partner.') % (st_line['name'], st_line['ref']))
elif len(invoice_id) == 1:
invoice = invoice_obj.browse(cr, uid, invoice_id[0],
context=context)
res['partner_id'] = invoice.partner_id.id
# we want the move to have the same ref than the found
# invoice's move, thus it will be easier to link them for the
# accountants
if invoice.move_id:
res['ref'] = invoice.move_id.ref
st_vals = st_obj.get_values_for_line(
cr, uid,
profile_id=st_line['profile_id'],
master_account_id=st_line['master_account_id'],
partner_id=res.get('partner_id', False),
line_type=st_line['type'],
amount=st_line['amount'] if st_line['amount'] else 0.0,
context=context)
res.update(st_vals)
return res
class AccountStatementLine(Model):
_inherit = "account.bank.statement.line"
@@ -92,3 +142,38 @@ class AccountStatementLine(Model):
serialization_field='additionnal_bank_fields',
help="Transaction id from the financial institute"),
}
class AccountBankStatement(Model):
_inherit = "account.bank.statement"
def _prepare_move_line_vals(
self, cr, uid, st_line, move_id, debit, credit, currency_id=False,
amount_currency=False, account_id=False, analytic_id=False,
partner_id=False, context=None):
"""Add the period_id from the statement line date to the move preparation.
Originaly, it was taken from the statement period_id
:param browse_record st_line: account.bank.statement.line record to
create the move from.
:param int/long move_id: ID of the account.move to link the move line
:param float debit: debit amount of the move line
:param float credit: credit amount of the move line
:param int/long currency_id: ID of currency of the move line to create
:param float amount_currency: amount of the debit/credit expressed in the currency_id
:param int/long account_id: ID of the account to use in the move line if different
from the statement line account ID
:param int/long analytic_id: ID of analytic account to put on the move line
:param int/long partner_id: ID of the partner to put on the move line
:return: dict of value to create() the account.move.line
"""
res = super(AccountBankStatement, self)._prepare_move_line_vals(
cr, uid, st_line, move_id, debit, credit,
currency_id=currency_id,
amount_currency=amount_currency,
account_id=account_id,
analytic_id=analytic_id,
partner_id=partner_id, context=context)
if st_line.transaction_id:
res['transaction_ref'] = st_line.transaction_id
return res

View File

@@ -7,13 +7,13 @@
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field eval="20" name="priority"/>
<field name="type">form</field>
<field name="arch" type="xml">
<data>
<xpath expr="/form/sheet/notebook/page/field[@name='line_ids']/form/group/field[@name='label']" position="after">
<field name="transaction_id" />
</xpath>
</data>
<xpath expr="//field[@name='line_ids']/form//field[@name='label']" position="after">
<field name="transaction_id" />
</xpath>
<xpath expr="//field[@name='line_ids']/tree/field[@name='ref']" position="after">
<field name="transaction_id" />
</xpath>
</field>
</record>

View File

@@ -0,0 +1,49 @@
-
In order to test the banking framework, I first need to create a profile
-
!record {model: account.statement.profile, id: statement_profile_invoice_transactionid}:
name: Bank EUR Profile (invoice transaction ID)
journal_id: account.bank_journal
commission_account_id: account.a_expense
company_id: base.main_company
balance_check: True
rule_ids:
- bank_statement_completion_rule_trans_id_invoice
-
Now I create a statement. I create statment lines separately because I need
to find each one by XML id
-
!record {model: account.bank.statement, id: statement_invoice_transactionid_test1}:
name: Statement with transaction ID
profile_id: statement_profile_invoice_transactionid
company_id: base.main_company
-
I create a statement line for an invoice with transaction ID
-
!record {model: account.bank.statement.line, id: statement_line_invoice_transactionid}:
name: Test autocompletion based on invoice with transaction ID
statement_id: statement_invoice_transactionid_test1
transaction_id: XXX77Z
ref: 6
date: !eval time.strftime('%Y-%m-%d')
amount: 450
-
I run the auto complete
-
!python {model: account.bank.statement}: |
result = self.button_auto_completion(cr, uid, [ref("statement_invoice_transactionid_test1")])
-
Now I can check that all is nice and shiny, line 1. I expect the invoice has been
recognised from the transaction ID.
-
!assert {model: account.bank.statement.line, id: statement_line_invoice_transactionid, string: Check completion by Invoice transaction ID}:
- partner_id.name == u'Agrolait'
-
I verify if the reference of the move has been copied to the statement line
-
!python {model: account.bank.statement.line}: |
statement_line = self.browse(cr, uid, ref('statement_line_invoice_transactionid'))
invoice_obj = self.pool['account.invoice']
invoice = invoice_obj.browse(cr, uid, ref('invoice_with_transaction_id'))
reference = invoice.move_id.ref
assert statement_line.ref == reference

View File

@@ -35,7 +35,7 @@
I run the auto complete
-
!python {model: account.bank.statement}: |
result = self.button_auto_completion(cr, uid, [ref("statement_profile_transactionid")])
result = self.button_auto_completion(cr, uid, [ref("statement_transactionid_test1")])
-
Now I can check that all is nice and shiny, line 1. I expect the SO has been
recognised from the transaction ID.

View File

@@ -0,0 +1,30 @@
-
I create a new invoice with transaction ID
-
!record {model: account.invoice, id: invoice_with_transaction_id}:
account_id: account.a_recv
company_id: base.main_company
currency_id: base.EUR
partner_id: base.res_partner_2
transaction_id: XXX77Z
invoice_line:
- account_id: account.a_sale
name: '[PCSC234] PC Assemble SC234'
price_unit: 450.0
quantity: 1.0
product_id: product.product_product_3
uos_id: product.product_uom_unit
journal_id: account.bank_journal
reference_type: none
-
I called the "Confirm Draft Invoices" wizard
-
!record {model: account.invoice.confirm, id: invoice_transaction_id_confirm}:
{}
-
I clicked on Confirm Invoices Button
-
!python {model: account.invoice.confirm}: |
self.invoice_confirm(cr, uid, [ref("invoice_transaction_id_confirm")], {"lang": 'en_US',
"tz": False, "active_model": "account.invoice", "active_ids": [ref("invoice_with_transaction_id")],
"type": "out_invoice", "active_id": ref("invoice_with_transaction_id"), })

View File

@@ -22,3 +22,4 @@
from . import invoice
from . import sale
from . import stock
from . import account_move

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
class account_move_line(orm.Model):
_inherit = 'account.move.line'
_columns = {
'transaction_ref': fields.char('Transaction Ref.',
select=True),
}
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
default['transaction_ref'] = False
return super(account_move_line, self).\
copy_data(cr, uid, id, default=default, context=context)

View File

@@ -29,8 +29,22 @@ class AccountInvoice(Model):
_columns = {
'transaction_id': fields.char(
'Transaction id',
size=128,
required=False,
select=1,
help="Transction id from the financial institute"),
help="Transaction id from the financial institute"),
}
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
default['transaction_id'] = False
return super(AccountInvoice, self).\
copy_data(cr, uid, id, default=default, context=context)
def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
if invoice_browse.transaction_id:
invoice_account_id = invoice_browse.account_id.id
for line in move_lines:
# tuple (0, 0, {values})
if invoice_account_id == line[2]['account_id']:
line[2]['transaction_ref'] = invoice_browse.transaction_id
return move_lines

View File

@@ -72,18 +72,23 @@ class AccountStatementFromInvoiceLines(orm.TransientModel):
s_type = 'customer'
elif line.journal_id.type in ('purchase', 'purhcase_refund'):
s_type = 'supplier'
statement_line_obj.create(cr, uid, {
'name': line.name or '?',
vals = self._prepare_statement_line_vals(
cr, uid, line, s_type, statement_id, amount, context=context)
statement_line_obj.create(cr, uid, vals, context=context)
return {'type': 'ir.actions.act_window_close'}
def _prepare_statement_line_vals(self, cr, uid, move_line, s_type,
statement_id, amount, context=None):
return {'name': move_line.name or '?',
'amount': amount,
'type': s_type,
'partner_id': line.partner_id.id,
'account_id': line.account_id.id,
'partner_id': move_line.partner_id.id,
'account_id': move_line.account_id.id,
'statement_id': statement_id,
'ref': line.ref,
'ref': move_line.ref,
'voucher_id': False,
'date': time.strftime('%Y-%m-%d'),
}, context=context)
return {'type': 'ir.actions.act_window_close'}
}
class AccountPaymentPopulateStatement(orm.TransientModel):
@@ -114,16 +119,23 @@ class AccountPaymentPopulateStatement(orm.TransientModel):
if not line.move_line_id.id:
continue
context.update({'move_line_ids': [line.move_line_id.id]})
st_line_id = statement_line_obj.create(cr, uid, {
'name': line.order_id.reference or '?',
'amount': - amount,
'type': 'supplier',
'partner_id': line.partner_id.id,
'account_id': line.move_line_id.account_id.id,
'statement_id': statement.id,
'ref': line.communication,
'date': line.date or line.ml_maturity_date or statement.date,
}, context=context)
vals = self._prepare_statement_line_vals(
cr, uid, line, -amount, statement, context=context)
st_line_id = statement_line_obj.create(cr, uid, vals,
context=context)
line_obj.write(cr, uid, [line.id], {'bank_statement_line_id': st_line_id})
return {'type': 'ir.actions.act_window_close'}
def _prepare_statement_line_vals(self, cr, uid, payment_line, amount,
statement, context=None):
return {'name': payment_line.order_id.reference or '?',
'amount': amount,
'type': 'supplier',
'partner_id': payment_line.partner_id.id,
'account_id': payment_line.move_line_id.account_id.id,
'statement_id': statement.id,
'ref': payment_line.communication,
'date': (payment_line.date or payment_line.ml_maturity_date or
statement.date)
}