mirror of
https://github.com/OCA/bank-payment.git
synced 2025-02-02 10:37:31 +02:00
[FIX] Convert the move line amount to the bank statement currency so that the residual amount is calculated correctly in multi-currency situations.
[ADD] Use a drop-down to select full reconcile vs. part reconcile, as per account_voucher. [ADD] Create an account.voucher from the reconciliation information and generate moves using standard account_voucher code to simplify the account_banking module. [TODO] Need to ensure all the necessary fields are gathered when a statement line is automatically matched. [TODO] Need to handle cancelations.
This commit is contained in:
@@ -1454,25 +1454,12 @@ class banking_import_transaction(osv.osv):
|
||||
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')
|
||||
res = {}
|
||||
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)
|
||||
res[transaction.id] = abs(transaction.transferred_amount) - abs(transaction.move_currency_amount)
|
||||
|
||||
return res
|
||||
|
||||
def _get_match_multi(self, cr, uid, ids, name, args, context=None):
|
||||
@@ -1517,6 +1504,33 @@ class banking_import_transaction(osv.osv):
|
||||
write_vals.update(vals or {})
|
||||
return self.write(cr, uid, ids, write_vals, context=context)
|
||||
|
||||
def _get_move_amount(self, cr, uid, ids, name, args, context=None):
|
||||
"""
|
||||
Need to get the residual amount on the move (invoice) in the bank statement currency.
|
||||
This will be used to calculate the write-off amount (in statement currency).
|
||||
"""
|
||||
if not ids:
|
||||
return {}
|
||||
|
||||
stline_pool = self.pool.get('account.bank.statement.line')
|
||||
|
||||
res = {}
|
||||
|
||||
for transaction in self.browse(cr, uid, ids, context):
|
||||
|
||||
if transaction.move_line_id:
|
||||
move_line_amount = transaction.move_line_id.amount_residual_currency
|
||||
to_curr_id = transaction.statement_id.journal_id.currency and transaction.statement_id.journal_id.currency.id or transaction.statement_line_id.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 transaction.statement_id.company_id.currency_id.id
|
||||
if from_curr_id != to_curr_id:
|
||||
amount_currency = stline_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
|
||||
res[transaction.id] = amount_currency
|
||||
|
||||
return res
|
||||
|
||||
column_map = {
|
||||
# used in bank_import.py, converting non-osv transactions
|
||||
'statement_id': 'statement',
|
||||
@@ -1604,18 +1618,22 @@ class banking_import_transaction(osv.osv):
|
||||
'writeoff_account_id': fields.many2one(
|
||||
'account.account', 'Write-off account',
|
||||
domain=[('type', '!=', 'view')]),
|
||||
'writeoff_journal_id': fields.many2one(
|
||||
'account.journal', 'Write-off journal'),
|
||||
'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'),
|
||||
'writeoff_move_line_id': fields.many2one(
|
||||
'account.move.line', 'Write off move line'),
|
||||
'writeoff_analytic_id': fields.many2one(
|
||||
'account.analytic.account', 'Write off analytic account'),
|
||||
'move_currency_amount': fields.function(
|
||||
_get_move_amount, method=True, string='Match Amount', type='float'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'company_id': lambda s,cr,uid,c:
|
||||
s.pool.get('res.company')._company_default_get(
|
||||
cr, uid, 'bank.import.transaction', context=c),
|
||||
'payment_option': 'without_writeoff',
|
||||
}
|
||||
|
||||
banking_import_transaction()
|
||||
@@ -1671,14 +1689,28 @@ class account_bank_statement_line(osv.osv):
|
||||
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')
|
||||
|
||||
|
||||
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:
|
||||
ctxt = context.copy()
|
||||
else:
|
||||
ctxt = {}
|
||||
if date:
|
||||
ctxt["date"] = date
|
||||
|
||||
amount = curr_obj.compute(cr, uid, from_curr_id, to_curr_id, from_amount, round=round, context=ctxt)
|
||||
return amount
|
||||
|
||||
|
||||
def confirm_with_voucher(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Create (or update) a voucher for each statement line, and then generate
|
||||
the moves by posting the voucher.
|
||||
"""
|
||||
voucher_pool = self.pool.get('account.voucher')
|
||||
|
||||
for st_line in self.browse(cr, uid, ids, context):
|
||||
if st_line.state != 'draft':
|
||||
@@ -1697,37 +1729,143 @@ class account_bank_statement_line(osv.osv):
|
||||
"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
|
||||
|
||||
# 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:
|
||||
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)
|
||||
# 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)
|
||||
|
||||
# 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()
|
||||
|
||||
# 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)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_voucher(self, cr, uid, ids, st_line, context):
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
else:
|
||||
writeoff = 0.0
|
||||
|
||||
# Define the voucher
|
||||
voucher = {
|
||||
'journal_id': st_line.statement_id.journal_id.id,
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'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,
|
||||
}
|
||||
|
||||
# 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': abs(amount_currency),
|
||||
'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
|
||||
|
||||
|
||||
def confirm(self, cr, uid, ids, context=None):
|
||||
return self.confirm_with_voucher(cr, uid, ids, context=context)
|
||||
# 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]
|
||||
|
||||
@@ -267,11 +267,12 @@ class IBAN(str):
|
||||
in 'C' implemented class, this can't be done in __init__.
|
||||
'''
|
||||
init = ''
|
||||
for item in arg.upper():
|
||||
if item.isalnum():
|
||||
init += item
|
||||
elif item not in ' \t.-':
|
||||
raise ValueError, 'Invalid chars found in IBAN number'
|
||||
if arg:
|
||||
for item in arg.upper():
|
||||
if item.isalnum():
|
||||
init += item
|
||||
elif item not in ' \t.-':
|
||||
raise ValueError, 'Invalid chars found in IBAN number'
|
||||
return str.__new__(cls, init)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -330,6 +330,8 @@ class banking_transaction_wizard(osv.osv_memory):
|
||||
domain=[('account_id.reconcile', '=', True),
|
||||
('reconcile_id', '=', False)],
|
||||
),
|
||||
'payment_option': fields.related('import_transaction_id','payment_option', string='Payment Difference', type='selection', required=True,
|
||||
selection=[('without_writeoff', 'Keep Open'),('with_writeoff', 'Reconcile Payment Balance')]),
|
||||
'writeoff_analytic_id': fields.related(
|
||||
'import_transaction_id', 'writeoff_analytic_id',
|
||||
type='many2one', relation='account.analytic.account',
|
||||
@@ -338,6 +340,8 @@ class banking_transaction_wizard(osv.osv_memory):
|
||||
'statement_line_id', 'analytic_account_id',
|
||||
type='many2one', relation='account.analytic.account',
|
||||
string="Analytic Account"),
|
||||
'move_currency_amount': fields.related('import_transaction_id','move_currency_amount',
|
||||
type='float', string='Match Currency Amount', readonly=True),
|
||||
#'manual_payment_order_id': fields.many2one(
|
||||
# 'payment.order', "Payment order to reconcile"),
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
<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"/>
|
||||
<separator string="Transaction data" colspan="4"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="date"/>
|
||||
<field name="ref"/>
|
||||
<field name="amount"/>
|
||||
</group>
|
||||
|
||||
<!-- (semi-) automatic matching and selection -->
|
||||
@@ -28,6 +28,9 @@
|
||||
|
||||
<separator string="Current match" colspan="4"/>
|
||||
<field name="match_type"/>
|
||||
<newline />
|
||||
<field name="move_currency_amount" />
|
||||
<newline />
|
||||
<field name="residual"/>
|
||||
<group attrs="{'invisible': [('match_multi', '=', False)]}" colspan="4" col="2">
|
||||
<separator string="Multiple matches" colspan="2"/>
|
||||
@@ -99,10 +102,10 @@
|
||||
</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"/>
|
||||
<field name="writeoff_account_id" />
|
||||
<field name="writeoff_journal_id" />
|
||||
<field name="writeoff_analytic_id" />
|
||||
<label string="Choose what you want to do with the eventual difference between the paid amount and the sum of allocated amounts. You can either choose to keep open this difference on the partner's account, or reconcile it with the payment." colspan="2"/>
|
||||
<field name="payment_option" />
|
||||
<field name="writeoff_account_id" attrs="{'required':[('payment_option','=','with_writeoff')],'invisible':[('payment_option','=','without_writeoff')]}" />
|
||||
<field name="writeoff_analytic_id" attrs="{'required':[('payment_option','=','with_writeoff')],'invisible':[('payment_option','=','without_writeoff')]}" />
|
||||
<button colspan="1"
|
||||
name="trigger_write"
|
||||
type="object"
|
||||
|
||||
Reference in New Issue
Block a user