[ADD] Move voucher logic from statement line to the transaction level

Preserve legacy methods for canceling reconciliation oriented transactions
This commit is contained in:
Stefan Rijnhart
2012-05-01 11:58:27 +02:00
parent d9c5b7ea06
commit 3f9ad78750

View File

@@ -534,9 +534,16 @@ class banking_import_transaction(osv.osv):
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)
def _create_voucher_move(self, cr, uid, transaction_id, context=None):
"""
The line is matched against a move (invoice), so generate a payment voucher with the write-off settings that the
user requested. The move lines will be generated by the voucher, handling rounding and currency conversion.
"""
if context is None:
context = {}
statement_line_pool = self.pool.get('account.bank.statement.line')
transaction = self.browse(cr, uid, transaction_id, context)
if not transaction.move_line_id:
if transaction.match_type == 'invoice':
raise osv.except_osv(
@@ -558,6 +565,87 @@ class banking_import_transaction(osv.osv):
transaction.statement_line_id.statement_id.name,
transaction.statement_line_id.name
)))
st_line = transaction.statement_line_id
journal = st_line.statement_id.journal_id
if st_line.amount < 0.0:
voucher_type = 'payment'
account_id = (journal.default_debit_account_id and
journal.default_debit_account_id.id or False)
else:
voucher_type = 'receipt'
account_id = (journal.default_credit_account_id and
journal.default_credit_account_id.id or False)
# Use the statement line's date determine the period
ctxt = context.copy()
ctxt['company_id'] = st_line.company_id.id
if 'period_id' in ctxt:
del ctxt['period_id']
period_id = self.pool.get('account.period').find(
cr, uid, st_line.date, context=ctxt)[0]
# Convert the move line amount to the journal currency
move_line_amount = transaction.move_line_id.amount_residual_currency
to_curr_id = (st_line.statement_id.journal_id.currency and
st_line.statement_id.journal_id.currency.id or
st_line.statement_id.company_id.currency_id.id)
from_curr_id = (transaction.move_line_id.currency_id and
transaction.move_line_id.currency_id.id or
st_line.statement_id.company_id.currency_id.id)
if from_curr_id != to_curr_id:
amount_currency = statement_line_pool._convert_currency(
cr, uid, from_curr_id, to_curr_id, move_line_amount,
round=True, date=time.strftime('%Y-%m-%d'),
context=context)
else:
amount_currency = move_line_amount
# Check whether this is a full or partial reconciliation
if transaction.payment_option=='with_writeoff':
writeoff = abs(st_line.amount)-abs(amount_currency)
line_amount = abs(amount_currency)
else:
writeoff = 0.0
line_amount = abs(st_line.amount)
# Define the voucher
voucher = {
'journal_id': st_line.statement_id.journal_id.id,
'partner_id': st_line.partner_id and st_line.partner_id.id or False,
'company_id': st_line.company_id.id,
'type':voucher_type,
'company_id': st_line.company_id.id,
'account_id': account_id,
'amount': abs(st_line.amount),
'writeoff_amount': writeoff,
'payment_option': transaction.payment_option,
'writeoff_acc_id': transaction.writeoff_account_id.id,
'analytic_id': transaction.writeoff_analytic_id.id,
'date': st_line.date,
'date_due': st_line.date,
'period_id': period_id,
'payment_rate_currency_id':to_curr_id,
}
# Define the voucher line
vch_line = {
#'voucher_id': v_id,
'move_line_id': transaction.move_line_id.id,
'reconcile': True,
'amount': line_amount,
'account_id': transaction.move_line_id.account_id.id,
'type': transaction.move_line_id.credit and 'dr' or 'cr',
}
voucher['line_ids'] = [(0,0,vch_line)]
v_id = self.pool.get('account.voucher').create(
cr, uid, voucher, context=context)
return v_id
def _reconcile_move(
self, cr, uid, transaction_id, context=None):
transaction = self.browse(cr, uid, transaction_id, context=context)
currency = transaction.statement_line_id.statement_id.currency
line_ids = [transaction.move_line_id.id]
if transaction.writeoff_move_line_id:
@@ -653,20 +741,101 @@ class banking_import_transaction(osv.osv):
transaction.statement_line_id.amount,
transaction.statement_line_id.currency)
def _cancel_move(
self, cr, uid, transaction_id, context=None):
statement_line_obj = self.pool.get('account.bank.statement.line')
transaction = self.browse(cr, uid, transaction_id, context=context)
def _legacy_do_move_unreconcile(self, cr, uid, move_line_ids, currency, context=None):
"""
Legacy method. Allow for canceling bank statement lines that
were confirmed using earlier versions of the interactive wizard branch.
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 _legacy_clear_up_writeoff(self, cr, uid, transaction, context=None):
"""
Legacy method to support upgrades older installations of the
interactive wizard branch. To be removed after 6.2
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
def _legacy_cancel_move(
self, cr, uid, transaction, context=None):
"""
Legacy method to support upgrades from older installations
of the interactive wizard branch.
"""
move_line_id = transaction.move_line_id.id
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)
self._do_move_unreconcile(
self._legacy_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)
def _cancel_voucher(
self, cr, uid, transaction_id, context=None):
voucher_pool = self.pool.get('account.voucher')
transaction = self.browse(cr, uid, transaction_id, context=context)
st_line = transaction.statement_line_id
if transaction.match_type:
if st_line.voucher_id:
# Cancel and delete the vouchers
voucher_pool.cancel_voucher(
cr, uid, [st_line.voucher_id.id], context=context)
voucher_pool.action_cancel_draft(
cr, uid, [st_line.voucher_id.id], context=context)
voucher_pool.unlink(
cr, uid, [st_line.voucher_id.id], context=context)
# Allow canceling of legacy entries
elif st_line.reconcile_id:
self._legacy_cancel_move(cr, uid, transaction, context=context)
return True
def _cancel_storno(
@@ -724,12 +893,13 @@ class banking_import_transaction(osv.osv):
cancel_map = {
'storno': _cancel_storno,
'invoice': _cancel_move,
'manual': _cancel_move,
'move': _cancel_move,
'invoice': _cancel_voucher,
'manual': _cancel_voucher,
'move': _cancel_voucher,
'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]
@@ -743,31 +913,33 @@ class banking_import_transaction(osv.osv):
_("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)
self._legacy_clear_up_writeoff(cr, uid, transaction, context=context)
if transaction.statement_line_id.move_id:
# We allow for people canceling and removing
# the associated payments, which can lead to confirmed
# statement lines without an associated move
account_move_obj.button_cancel(
cr, uid, [transaction.statement_line_id.move_id], context)
account_move_obj.unlink(
cr, uid, [transaction.statement_line_id.move_id], 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,
create_voucher_map = {
# 'storno': _create_voucher_storno,
'invoice': _create_voucher_move,
'manual': _create_voucher_move,
# 'payment_order': _create_voucher_payment_order,
# 'payment': _create_voucher_payment,
'move': _create_voucher_move,
}
def reconcile(self, cr, uid, ids, context=None):
def create_voucher(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:
if transaction.match_type not in self.create_voucher_map:
raise osv.except_osv(
_("Cannot reconcile"),
_("Cannot reconcile type %s. No method found to " +
@@ -782,14 +954,11 @@ class banking_import_transaction(osv.osv):
"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](
voucher_id = self.create_voucher_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)
{'voucher_id': voucher_id}, context=context)
# TODO
# update the statement line bank account reference
@@ -802,64 +971,7 @@ class banking_import_transaction(osv.osv):
][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):
# Get the period for the company and date
ctxt = context.copy()
ctxt['company_id'] = trans.company_id.id
periods = self.pool.get('account.period').find(
cr, uid, trans.statement_line_id.date, context=ctxt)
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,
'analytic_account_id': trans.writeoff_analytic_id.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')
@@ -1618,9 +1730,20 @@ class banking_import_transaction(osv.osv):
'writeoff_account_id': fields.many2one(
'account.account', 'Write-off account',
domain=[('type', '!=', 'view')]),
'payment_option':fields.selection([('without_writeoff', 'Keep Open'),('with_writeoff', 'Reconcile Payment Balance')], 'Payment Difference',
required=True, help="This field helps you to choose what you want to do with the eventual difference between the paid amount and the sum of allocated amounts. You can either choose to keep open this difference on the partner's account, or reconcile it with the payment(s)"),
'payment_option':fields.selection(
[
('without_writeoff', 'Keep Open'),
('with_writeoff', 'Reconcile Payment Balance')
], 'Payment Difference',
required=True,
help=("This field helps you to choose what you want to do with "
"the eventual difference between the paid amount and the "
"sum of allocated amounts. You can either choose to keep "
"open this difference on the partner's account, "
"or reconcile it with the payment(s)"),
),
'writeoff_amount': fields.float('Difference Amount'),
# Legacy field: to be removed after 6.2
'writeoff_move_line_id': fields.many2one(
'account.move.line', 'Write off move line'),
'writeoff_analytic_id': fields.many2one(
@@ -1690,8 +1813,9 @@ class account_bank_statement_line(osv.osv):
res = wizard_obj.create_act_window(cr, uid, res_id, context=context)
return res
def _convert_currency(self, cr, uid, from_curr_id, to_curr_id, from_amount, round=False, date=None, context=None):
def _convert_currency(
self, cr, uid, from_curr_id, to_curr_id, from_amount,
round=False, date=None, context=None):
"""Convert currency amount using the company rate on a specific date"""
curr_obj = self.pool.get('res.currency')
if context:
@@ -1701,10 +1825,11 @@ class account_bank_statement_line(osv.osv):
if date:
ctxt["date"] = date
amount = curr_obj.compute(cr, uid, from_curr_id, to_curr_id, from_amount, round=round, context=ctxt)
amount = curr_obj.compute(
cr, uid, from_curr_id, to_curr_id, from_amount,
round=round, context=ctxt)
return amount
def confirm(self, cr, uid, ids, context=None):
"""
Create (or update) a voucher for each statement line, and then generate
@@ -1716,6 +1841,7 @@ class account_bank_statement_line(osv.osv):
statement_pool = self.pool.get('account.bank.statement')
obj_seq = self.pool.get('ir.sequence')
move_pool = self.pool.get('account.move')
import_transaction_obj = self.pool.get('banking.import.transaction')
for st_line in self.browse(cr, uid, ids, context):
if st_line.state != 'draft':
@@ -1747,10 +1873,15 @@ class account_bank_statement_line(osv.osv):
st_number = obj_seq.next_by_code(cr, uid, 'account.bank.statement')
statement_obj.write(cr, uid, [st.id], {'name': st_number}, context=context)
# Check if this line has been matched against a journal item
if st_line.import_transaction_id.move_line_id:
if st_line.import_transaction_id:
import_transaction_obj.create_voucher(
cr, uid, st_line.import_transaction_id.id, context)
st_line.refresh()
if st_line.voucher_id:
# Check if this line has been matched against a journal item
# Line has been matched, so post it via a voucher
self._post_with_voucher(cr, uid, ids, st_line, voucher_pool, move_pool, context=context)
self._post_voucher(cr, uid, ids, st_line, move_pool, context=context)
else:
# Check to see if the line has an account that can be used to generate a journal entry
@@ -1776,104 +1907,18 @@ class account_bank_statement_line(osv.osv):
self.write(cr, uid, st_line.id, {'state': 'confirmed', 'move_id': move_id}, context)
def _post_with_voucher(self, cr, uid, ids, st_line, voucher_pool, move_pool, context):
# Check if a voucher has already been created
if st_line.voucher_id:
# There's an existing voucher on the statement line which was probably created
# manually. This may have been done because it was a single payment for multiple
# invoices. Just get the voucher ID.
voucher_id = st_line.voucher_id.id
else:
# Create a voucher and store the ID on the statement line
voucher_id = self._create_voucher(cr, uid, ids, st_line, context=context)
self.pool.get('account.bank.statement.line').write(cr,uid,st_line.id,{'voucher_id':voucher_id} , context=context)
def _post_voucher(self, cr, uid, ids, st_line, move_pool, context):
# If the voucher is in draft mode, then post it
voucher = voucher_pool.browse(cr, uid, voucher_id, context=context)
if voucher.state in ('draft','proforma'):
voucher.action_move_line_create()
if st_line.voucher_id.state in ('draft','proforma'):
st_line.voucher_id.action_move_line_create()
st_line.voucher_id.refresh()
# Update the statement line to indicate that it has been posted
# ... no longer need to set the move_id on the voucher?
self.write(cr, uid, st_line.id, {'state': 'confirmed'}, context)
# The voucher journal isn't automatically posted, so post it (if needed)
if not voucher.journal_id.entry_posted:
voucher = voucher_pool.browse(cr, uid, voucher_id, context=context)
move_pool.post(cr, uid, [voucher.move_id.id], context={})
def _create_voucher(self, cr, uid, ids, st_line, context):
"""
The line is matched against a move (invoice), so generate a payment voucher with the write-off settings that the
user requested. The move lines will be generated by the voucher, handling rounding and currency conversion.
"""
journal = st_line.statement_id.journal_id
if st_line.amount < 0.0:
voucher_type = 'payment'
account_id = journal.default_debit_account_id and journal.default_debit_account_id.id or False
else:
voucher_type = 'receipt'
account_id = journal.default_credit_account_id and journal.default_credit_account_id.id or False
# Use the statement line's date determine the period
ctxt = context.copy()
ctxt['company_id'] = st_line.company_id.id
if 'period_id' in ctxt:
del ctxt['period_id']
period_id = self.pool.get('account.period').find(cr, uid, st_line.date, context=ctxt)[0]
# Convert the move line amount to the journal currency
move_line_amount = st_line.import_transaction_id.move_line_id.amount_residual_currency
to_curr_id = st_line.statement_id.journal_id.currency and st_line.statement_id.journal_id.currency.id or st_line.statement_id.company_id.currency_id.id
from_curr_id = st_line.import_transaction_id.move_line_id.currency_id and st_line.import_transaction_id.move_line_id.currency_id.id or st_line.statement_id.company_id.currency_id.id
if from_curr_id != to_curr_id:
amount_currency = self._convert_currency(cr, uid, from_curr_id, to_curr_id, move_line_amount, round=True,
date=time.strftime('%Y-%m-%d'), context=context)
else:
amount_currency = move_line_amount
# Check whether this is a full or partial reconciliation
if st_line.import_transaction_id.payment_option=='with_writeoff':
writeoff = abs(st_line.amount)-abs(amount_currency)
line_amount = abs(amount_currency)
else:
writeoff = 0.0
line_amount = abs(st_line.amount)
# Define the voucher
voucher = {
'journal_id': st_line.statement_id.journal_id.id,
'partner_id': st_line.partner_id and st_line.partner_id.id or False,
'company_id': st_line.company_id.id,
'type':voucher_type,
'company_id': st_line.company_id.id,
'account_id': account_id,
'amount': abs(st_line.amount),
'writeoff_amount': writeoff,
'payment_option': st_line.import_transaction_id.payment_option,
'writeoff_acc_id': st_line.import_transaction_id.writeoff_account_id.id,
'analytic_id': st_line.import_transaction_id.writeoff_analytic_id.id,
'date': st_line.date,
'date_due': st_line.date,
'period_id': period_id,
'payment_rate_currency_id':to_curr_id,
}
# Define the voucher line
vch_line = {
#'voucher_id': v_id,
'move_line_id': st_line.import_transaction_id.move_line_id.id,
'reconcile': True,
'amount': line_amount,
'account_id': st_line.import_transaction_id.move_line_id.account_id.id,
'type': st_line.import_transaction_id.move_line_id.credit and 'dr' or 'cr',
}
voucher['line_ids'] = [(0,0,vch_line)]
v_id = self.pool.get('account.voucher').create(cr, uid, voucher, context=context)
return v_id
if not st_line.voucher_id.journal_id.entry_posted:
move_pool.post(cr, uid, [st_line.voucher_id.move_id.id], context={})
def cancel(self, cr, uid, ids, context=None):
if ids and isinstance(ids, (int, float)):
@@ -1895,15 +1940,10 @@ class account_bank_statement_line(osv.osv):
_("The bank statement that this transaction belongs to has "
"already been confirmed"))
# Check if the transaction has a voucher
if st_line.voucher_id:
voucher_cancel_ids.append(st_line.voucher_id.id)
if st_line.import_transaction_id:
transaction_cancel_ids.append(st_line.import_transaction_id.id)
else:
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:
if not st_line.move_id:
raise osv.except_osv(
_("Cannot cancel bank transaction"),
_("Cannot cancel this bank transaction. The information "
@@ -1911,16 +1951,9 @@ class account_bank_statement_line(osv.osv):
"recorded"))
set_draft_ids.append(st_line.id)
# Cancel and delete the vouchers
voucher_pool.cancel_voucher(cr, uid, voucher_cancel_ids, context=context)
voucher_pool.action_cancel_draft(cr, uid, voucher_cancel_ids, context=context)
voucher_pool.unlink(cr, uid, voucher_cancel_ids, context=context)
# 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