[FIX] do an actual partial reconciliation at statement confirm time

[FIX] no context in match_invoice()
[FIX] transaction update and browse again after split
[ADD] transaction parent_id relation for split records
[ADD] residual and write-off infrastructure
[ADD] option to disable reconciliation on bank transaction
This commit is contained in:
OpenERP instance user
2011-12-19 21:58:28 +01:00
parent 3f4c0a7d44
commit 908ade7d5c
4 changed files with 154 additions and 39 deletions

View File

@@ -453,7 +453,9 @@ class account_bank_statement(osv.osv):
""" """
if st_line.reconcile_id: if st_line.reconcile_id:
account_move_line_obj.write(cr, uid, [torec], { account_move_line_obj.write(cr, uid, [torec], {
'reconcile_id': st_line.reconcile_id.id }, context=context) (st_line.reconcile_id.line_partial_ids and
'reconcile_partial_id' or 'reconcile_id'):
st_line.reconcile_id.id }, context=context)
for move_line in (st_line.reconcile_id.line_id or []) + ( for move_line in (st_line.reconcile_id.line_id or []) + (
st_line.reconcile_id.line_partial_ids or []): st_line.reconcile_id.line_partial_ids or []):
netsvc.LocalService("workflow").trg_trigger( netsvc.LocalService("workflow").trg_trigger(

View File

@@ -171,7 +171,8 @@ class banking_import_transaction(osv.osv):
return False return False
def _match_invoice(self, cr, uid, trans, move_lines, def _match_invoice(self, cr, uid, trans, move_lines,
partner_ids, bank_account_ids, log, linked_invoices): partner_ids, bank_account_ids, log, linked_invoices,
context=None):
''' '''
Find the invoice belonging to this reference - if there is one Find the invoice belonging to this reference - if there is one
Use the sales journal to check. Use the sales journal to check.
@@ -405,17 +406,15 @@ class banking_import_transaction(osv.osv):
dict( dict(
transferred_amount = trans.transferred_amount - expected, transferred_amount = trans.transferred_amount - expected,
transaction = trans.transaction + 'b', transaction = trans.transaction + 'b',
parent_id = trans.id,
), context=context) ), context=context)
# update the current record
self.write(cr, uid, trans.id, dict( self.write(cr, uid, trans.id, dict(
transaction = trans.transaction + 'a'), context) transferred_amount = expected,
# NOTE: the following is debatable. By copying the transaction = trans.transaction + 'a',
# eyecatcher of the invoice itself, we enhance the ), context)
# tracability of the invoices, but we degrade the # rebrowse the current record after writing
# tracability of the bank transactions. When debugging, it trans=self.browse(cr, uid, trans.id, context=context)
# is wise to disable this line.
# Stefan: disabled for interactive mode
# trans.reference = eyecatcher(move_line.invoice)
if move_line: if move_line:
account_ids = [ account_ids = [
x.id for x in bank_account_ids x.id for x in bank_account_ids
@@ -691,8 +690,16 @@ class banking_import_transaction(osv.osv):
continue continue
if transaction.match_type not in self.reconcile_map: if transaction.match_type not in self.reconcile_map:
raise osv.except_osv( raise osv.except_osv(
_("Cannot reconcile type %s" % transaction.match_type), _("Cannot reconcile"),
_("No method found to reconcile this type")) _("Cannot reconcile type %s. No method found to reconcile this type") %
transaction.match_type
)
if transaction.residual and transaction.writeoff_account_id:
raise osv.except_osv(
_("Cannot reconcile"),
_("Bank transaction %s has a residual amount, and a write-off account. Write off not implemented.") %
transaction.statement_line_id.name
)
# run the method that is appropriate for this match type # run the method that is appropriate for this match type
reconcile_id = self.reconcile_map[transaction.match_type]( reconcile_id = self.reconcile_map[transaction.match_type](
self, cr, uid, transaction.id, context) self, cr, uid, transaction.id, context)
@@ -1049,6 +1056,7 @@ class banking_import_transaction(osv.osv):
transferred_amount = transaction.provision_costs, transferred_amount = transaction.provision_costs,
remote_currency = transaction.provision_costs_currency, remote_currency = transaction.provision_costs_currency,
message = transaction.provision_costs_description, message = transaction.provision_costs_description,
parent_id = transaction.id,
), context) ), context)
injected.append(self.browse(cr, uid, cost_id, context)) injected.append(self.browse(cr, uid, cost_id, context))
@@ -1066,7 +1074,8 @@ class banking_import_transaction(osv.osv):
provision_costs_currency = False, provision_costs_currency = False,
provision_costs_description = False, provision_costs_description = False,
), context=context) ), context=context)
# rebrowse the current record after writing
transaction=self.browse(cr, uid, transaction.id, context=context)
# Match full direct debit orders # Match full direct debit orders
if transaction.type == bt.DIRECT_DEBIT: if transaction.type == bt.DIRECT_DEBIT:
move_info = self._match_debit_order( # TODO reconcile preservation move_info = self._match_debit_order( # TODO reconcile preservation
@@ -1154,9 +1163,9 @@ class banking_import_transaction(osv.osv):
move_info, remainder = self._match_invoice( move_info, remainder = self._match_invoice(
cr, uid, transaction, move_lines, partner_ids, cr, uid, transaction, move_lines, partner_ids,
partner_banks, results['log'], linked_invoices, partner_banks, results['log'], linked_invoices,
) context=context)
if remainder: if remainder:
injected.append(remainder) injected.append(self.browse(cr, uid, remainder, context))
account_id = move_info and move_info.get('account_id', False) account_id = move_info and move_info.get('account_id', False)
if not account_id: if not account_id:
@@ -1269,6 +1278,38 @@ class banking_import_transaction(osv.osv):
'id': 'transaction' 'id': 'transaction'
} }
def _get_residual(self, cr, uid, ids, name, args, context=None):
"""
Calculate the residual against the candidate reconciliation.
When
- 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.match_type in
[('invoice'), ('move'), ('manual')]):
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): def _get_match_multi(self, cr, uid, ids, name, args, context=None):
""" """
Indicate in the wizard that multiple matches have been found Indicate in the wizard that multiple matches have been found
@@ -1333,11 +1374,13 @@ class banking_import_transaction(osv.osv):
'account.bank.statement.line', 'Statement line'), 'account.bank.statement.line', 'Statement line'),
'statement_id': fields.many2one( 'statement_id': fields.many2one(
'account.bank.statement', 'Statement'), 'account.bank.statement', 'Statement'),
'parent_id': fields.many2one(
'banking.import.transaction', 'Split off from this transaction'),
# match fields # match fields
'match_type': fields.selection( 'match_type': fields.selection(
[('move','Move'), ('invoice', 'Invoice'), ('payment', 'Payment'), [('manual', 'Manual'), ('move','Move'), ('invoice', 'Invoice'),
('payment_order', 'Payment order'), ('storno', 'Storno')], ('payment', 'Payment'), ('payment_order', 'Payment order'),
('storno', 'Storno')],
'Match type'), 'Match type'),
'match_multi': fields.function( 'match_multi': fields.function(
_get_match_multi, method=True, string='Multi match', _get_match_multi, method=True, string='Multi match',
@@ -1359,6 +1402,12 @@ class banking_import_transaction(osv.osv):
'invoice_id': fields.many2one( 'invoice_id': fields.many2one(
'account.invoice', 'Invoice to reconcile'), 'account.invoice', 'Invoice to reconcile'),
'payment_line_id': fields.many2one('payment.line', 'Payment line'), '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'),
'writeoff_move_line_id': fields.many2one(
'account.move.line', 'Write off move line'),
} }
_defaults = { _defaults = {
'company_id': lambda s,cr,uid,c: 'company_id': lambda s,cr,uid,c:
@@ -1377,6 +1426,9 @@ class account_bank_statement_line(osv.osv):
'match_multi': fields.related( 'match_multi': fields.related(
'import_transaction_id', 'match_multi', type='boolean', 'import_transaction_id', 'match_multi', type='boolean',
string='Multi match'), string='Multi match'),
'residual': fields.related(
'import_transaction_id', 'residual', type='float',
string='Residual'),
'duplicate': fields.related( 'duplicate': fields.related(
'import_transaction_id', 'duplicate', type='boolean', 'import_transaction_id', 'duplicate', type='boolean',
string='Possible duplicate import'), string='Possible duplicate import'),

View File

@@ -113,12 +113,29 @@ class banking_transaction_wizard(osv.osv_memory):
_("No entry found for the selected invoice. " + _("No entry found for the selected invoice. " +
"Try manual reconciliation.")) "Try manual reconciliation."))
def select_match(self, cr, uid, ids, context=None): def trigger_write(self, cr, uid, ids, context=None):
""" """
Just a button that triggers a write. Just a button that triggers a write.
""" """
return True return True
def disable_match(self, cr, uid, ids, context=None):
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',
]]))
self.write(cr, uid, ids, vals, context=context)
return True
def reverse_duplicate(self, cr, uid, ids, context=None): def reverse_duplicate(self, cr, uid, ids, context=None):
if isinstance(ids, (int, float)): if isinstance(ids, (int, float)):
ids = [ids] ids = [ids]
@@ -144,7 +161,7 @@ class banking_transaction_wizard(osv.osv_memory):
return {'nodestroy': False, 'type': 'ir.actions.act_window_close'} return {'nodestroy': False, 'type': 'ir.actions.act_window_close'}
_defaults = { _defaults = {
'match_type': _get_default_match_type, # 'match_type': _get_default_match_type,
} }
_columns = { _columns = {
@@ -162,6 +179,10 @@ class banking_transaction_wizard(osv.osv_memory):
'residual': fields.related( 'residual': fields.related(
'import_transaction_id', 'residual', type='float', 'import_transaction_id', 'residual', type='float',
string='Residual'), string='Residual'),
'writeoff_account_id': fields.related(
'import_transaction_id', 'writeoff_account_id',
type='many2one', relation='account.account',
string='Write off account'),
'payment_line_id': fields.related( 'payment_line_id': fields.related(
'import_transaction_id', 'payment_line_id', string="Matching payment or storno", 'import_transaction_id', 'payment_line_id', string="Matching payment or storno",
type='many2one', relation='payment.line'), type='many2one', relation='payment.line'),
@@ -189,10 +210,9 @@ class banking_transaction_wizard(osv.osv_memory):
'match_multi': fields.related( 'match_multi': fields.related(
'import_transaction_id', 'match_multi', 'import_transaction_id', 'match_multi',
type="boolean", string='Multiple matches'), type="boolean", string='Multiple matches'),
'match_type': fields.selection( 'match_type': fields.related(
[('manual', 'Manual'), ('move','Move'), ('invoice', 'Invoice'), 'import_transaction_id', 'match_type',
('payment', 'Payment'), ('payment_order', 'Payment order'), type="char", size=16, string='Match type', readonly=True),
('storno', 'Storno')], 'Match type', readonly=True),
'manual_invoice_id': fields.many2one( 'manual_invoice_id': fields.many2one(
'account.invoice', 'Match this invoice', 'account.invoice', 'Match this invoice',
domain=[('state', '=', 'open')]), domain=[('state', '=', 'open')]),

View File

@@ -15,6 +15,8 @@
<field name="duplicate" invisible="True"/> <field name="duplicate" invisible="True"/>
<group colspan="2" col="2"> <group colspan="2" col="2">
<!-- Duplicate flagging -->
<group colspan="2" col="2" attrs="{'invisible': [('duplicate', '=', False)]}"> <group colspan="2" col="2" attrs="{'invisible': [('duplicate', '=', False)]}">
<separator string="Duplicate flag"/> <separator string="Duplicate flag"/>
<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."/> <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."/>
@@ -24,37 +26,42 @@
type="object" type="object"
string="Remove duplicate flag"/> string="Remove duplicate flag"/>
</group> </group>
<!-- (semi-) automatic matching and selection -->
<group colspan="2" col="2"> <group colspan="2" col="2">
<separator string="System Match" colspan="2"/> <separator string="System Match" colspan="2"/>
<field name="match_type"/> <field name="match_type"/>
<group attrs="{'invisible': [('match_multi', '=', False)]}"> <group attrs="{'invisible': [('match_multi', '=', False)]}">
<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." /> <label colspan="2" string="Multiple matches were found for this bank transfer. You must pick one of the matches or select a match manually below." />
</group> </group>
<field name='payment_line_id' <field name='payment_line_id'
attrs="{'invisible': [('match_type', 'not in', [('payment', 'storno')])]}"/> attrs="{'invisible': [('match_type', 'not in', [('payment', 'storno')])]}"
/>
<group attrs="{'readonly': [('match_multi', '!=' True)]}"> <group attrs="{'readonly': [('match_multi', '!=' True)]}">
<field name='invoice_id' <field name='invoice_id'
attrs="{'invisible': [('match_type', '!=', 'invoice')]}" attrs="{'invisible': [('match_type', '!=', 'invoice')]}"
domain="[('id', 'in', invoice_ids[0][2])]"/> domain="[('id', 'in', invoice_ids[0][2])]"
/>
<field name='move_line_id' <field name='move_line_id'
attrs="{'invisible': [('match_type', '!=', 'move')]}"/> attrs="{'invisible': [('match_type', '!=', 'move')]}"
domain="[('id', 'in', move_line_ids[0][2])]"
/>
<field name='payment_order_id' <field name='payment_order_id'
attrs="{'invisible': [('match_type', '!=', 'payment_order')]}"/> attrs="{'invisible': [('match_type', '!=', 'payment_order')]}"
domain="[('id', 'in', payment_order_ids[0][2])]"
/>
</group> </group>
<button colspan="1" <button colspan="1"
name="select_match" name="trigger_write"
type="object" type="object"
attrs="{'invisible': [('match_multi', '=', False)]}" attrs="{'invisible': [('match_multi', '=', False)]}"
string="Select"/> string="Select"/>
<newline/> <newline/>
<separator string="Retry system match"/>
<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"/> <!-- Manual selection -->
<newline/>
<button colspan="1"
name="trigger_match"
type="object"
string="Match again"/>
</group>
<separator string="Manual match" colspan="2"/> <separator string="Manual match" colspan="2"/>
<field name="manual_invoice_id"/> <field name="manual_invoice_id"/>
<field name="manual_move_line_id"/> <field name="manual_move_line_id"/>
@@ -63,6 +70,40 @@
name="match_manual" name="match_manual"
type="object" type="object"
string="Match"/> string="Match"/>
</group>
<!-- residual and write off -->
<group attrs="{'invisible': [('residual', '=', False)]}" colspan="2" col="2">
<separator string="Residual write-off"/>
<field name="residual"/><field name="writeoff_account_id"/>
<label string="You can set a write-off account for the residual of this reconciliation" colspan="2"/>
<button colspan="1"
name="trigger_write"
type="object"
string="Set write-off account"/>
</group>
<!-- Redo automatic match -->
<group colspan="2" col="2">
<separator string="Retry system match"/>
<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"/>
</group>
<group attrs="{'invisible': [('match_type', '==', False)]}" colspan="2" col="2">
<separator string="Disable reconciliation"/>
<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>
<group colspan="2"> <group colspan="2">
<separator/> <separator/>
<button icon="gtk-ok" string="Done" special="cancel"/> <button icon="gtk-ok" string="Done" special="cancel"/>