mirror of
https://github.com/OCA/bank-payment.git
synced 2025-02-02 10:37:31 +02:00
[FIX] invoice reconciliation shows both full and partial lines [FIX] exception not raised [RFR] write trigger on match wizard invoice selection [ADD] filter invoices for direct debit order by payment term
455 lines
20 KiB
Python
455 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
import time
|
|
from osv import osv, fields
|
|
import netsvc
|
|
from tools.translate import _
|
|
|
|
class payment_mode(osv.osv):
|
|
_inherit = 'payment.mode'
|
|
_columns = {
|
|
'transfer_account_id': fields.many2one(
|
|
'account.account', 'Transfer account',
|
|
domain=[('type', '=', 'other'),
|
|
('reconcile', '=', True)],
|
|
help=('Pay off lines in sent orders with a ' +
|
|
'move on this account. For debit type modes only'),
|
|
),
|
|
'transfer_journal_id': fields.many2one(
|
|
'account.journal', 'Transfer journal',
|
|
help=('Journal to write payment entries when confirming ' +
|
|
'a debit order of this mode'),
|
|
),
|
|
# 'reference_filter': fields.char(
|
|
# 'Reference filter', size=16,
|
|
# help=(
|
|
# 'Optional substring filter on move line references. ' +
|
|
# 'You can use this in combination with a specific journal ' +
|
|
# 'for items that you want to handle with this mode. Use ' +
|
|
# 'a separate sequence for the journal with a distinguished ' +
|
|
# 'prefix or suffix and enter that character string here.'),
|
|
# ),
|
|
'payment_term_ids': fields.many2many(
|
|
'account.payment.term', 'account_payment_order_terms_rel',
|
|
'mode_id', 'term_id', 'Payment terms',
|
|
help=('Limit selected invoices to invoices with these payment ' +
|
|
'terms')
|
|
),
|
|
}
|
|
payment_mode()
|
|
|
|
class payment_order(osv.osv):
|
|
_inherit = 'payment.order'
|
|
|
|
def fields_view_get(self, cr, user, view_id=None, view_type='form',
|
|
context=None, toolbar=False, submenu=False):
|
|
"""
|
|
Pretty awful workaround for no dynamic domain possible on
|
|
widget='selection'
|
|
|
|
The domain is encoded in the context
|
|
|
|
Uhmm, are these results are cached?
|
|
"""
|
|
if not context:
|
|
context = {}
|
|
res = super(payment_order, self).fields_view_get(
|
|
cr, user, view_id, view_type, context, toolbar, submenu)
|
|
if context.get('search_payment_order_type', False) and view_type == 'form':
|
|
if 'mode' in res['fields'] and 'selection' in res['fields']['mode']:
|
|
mode_obj = self.pool.get('payment.mode')
|
|
domain = ['|', ('type', '=', False), ('type.payment_order_type', '=',
|
|
context['search_payment_order_type'])]
|
|
# the magic is in the value of the selection
|
|
res['fields']['mode']['selection'] = mode_obj._name_search(
|
|
cr, user, args=domain)
|
|
# also update the domain
|
|
res['fields']['mode']['domain'] = domain
|
|
return res
|
|
|
|
def debit_reconcile_transfer(self, cr, uid, payment_order_id,
|
|
amount, log, context=None):
|
|
"""
|
|
During import of bank statements, create the reconcile on the transfer
|
|
account containing all the move lines on the transfer account.
|
|
"""
|
|
move_line_obj = self.pool.get('account.move.line')
|
|
|
|
def is_zero(move_line, total):
|
|
return self.pool.get('res.currency').is_zero(
|
|
cr, uid, move_line.company_id.currency_id, total)
|
|
|
|
order = self.browse(cr, uid, payment_order_id, context)
|
|
line_ids = []
|
|
reconcile_id = False
|
|
for order_line in order.line_ids:
|
|
for line in order_line.debit_move_line_id.move_id.line_id:
|
|
if line.account_id.type == 'other' and not line.reconcile_id:
|
|
line_ids.append(line.id)
|
|
if is_zero(order.line_ids[0].debit_move_line_id,
|
|
move_line_obj.get_balance(cr, uid, line_ids) - amount):
|
|
reconcile_id = self.pool.get('account.move.reconcile').create(
|
|
cr, uid,
|
|
{'type': 'auto', 'line_id': [(6, 0, line_ids)]},
|
|
context)
|
|
# set direct debit order to finished state
|
|
wf_service = netsvc.LocalService('workflow')
|
|
wf_service.trg_validate(
|
|
uid, 'payment.order', payment_order_id, 'done', cr)
|
|
|
|
return reconcile_id
|
|
|
|
def action_sent(self, cr, uid, ids, context=None):
|
|
"""
|
|
Create the moves that pay off the move lines from
|
|
the debit order. This happens when the debit order file is
|
|
generated.
|
|
"""
|
|
res = super(payment_order, self).action_sent(
|
|
cr, uid, ids, context)
|
|
|
|
account_move_obj = self.pool.get('account.move')
|
|
account_move_line_obj = self.pool.get('account.move.line')
|
|
payment_line_obj = self.pool.get('payment.line')
|
|
for order in self.browse(cr, uid, ids, context=context):
|
|
if order.payment_order_type != 'debit':
|
|
continue
|
|
for line in order.line_ids:
|
|
# basic checks
|
|
if not line.move_line_id:
|
|
raise osv.except_osv(
|
|
_('Error'),
|
|
_('No move line provided for line %s') % line.name)
|
|
if line.move_line_id.reconcile_id:
|
|
raise osv.except_osv(
|
|
_('Error'),
|
|
_('Move line %s has already been paid/reconciled') %
|
|
line.move_line_id.name
|
|
)
|
|
|
|
move_id = account_move_obj.create(cr, uid, {
|
|
'journal_id': order.mode.transfer_journal_id.id,
|
|
'name': 'Debit order %s' % line.move_line_id.move_id.name,
|
|
'reference': 'DEB%s' % line.move_line_id.move_id.name,
|
|
}, context=context)
|
|
|
|
# TODO: multicurrency
|
|
|
|
# create the debit move line on the transfer account
|
|
vals = {
|
|
'name': 'Debit order for %s' % (
|
|
line.move_line_id.invoice and
|
|
line.move_line_id.invoice.number or
|
|
line.move_line_id.name),
|
|
'move_id': move_id,
|
|
'partner_id': line.partner_id.id,
|
|
'account_id': order.mode.transfer_account_id.id,
|
|
'credit': 0.0,
|
|
'debit': line.amount,
|
|
'date': time.strftime('%Y-%m-%d'),
|
|
}
|
|
transfer_move_line_id = account_move_line_obj.create(
|
|
cr, uid, vals, context=context)
|
|
|
|
# create the debit move line on the receivable account
|
|
vals.update({
|
|
'account_id': line.move_line_id.account_id.id,
|
|
'credit': line.amount,
|
|
'debit': 0.0,
|
|
})
|
|
reconcile_move_line_id = account_move_line_obj.create(
|
|
cr, uid, vals, context=context)
|
|
|
|
# register the debit move line on the payment line
|
|
# and call reconciliation on it
|
|
payment_line_obj.write(
|
|
cr, uid, line.id,
|
|
{'debit_move_line_id': reconcile_move_line_id},
|
|
context=context)
|
|
|
|
payment_line_obj.debit_reconcile(
|
|
cr, uid, line.id, context=context)
|
|
account_move_obj.post(cr, uid, [move_id], context=context)
|
|
return res
|
|
|
|
payment_order()
|
|
|
|
class payment_line(osv.osv):
|
|
_inherit = 'payment.line'
|
|
|
|
def debit_storno(self, cr, uid, payment_line_id, amount,
|
|
currency, storno_retry=True, context=None):
|
|
"""
|
|
The processing of a storno is triggered by a debit
|
|
transfer on one of the company's bank accounts.
|
|
This method offers to re-reconcile the original debit
|
|
payment. For this purpose, we have registered that
|
|
payment move on the payment line.
|
|
|
|
Return the (now incomplete) reconcile id. The caller MUST
|
|
re-reconcile this reconcile with the bank transfer and
|
|
re-open the associated invoice.
|
|
|
|
:param payment_line_id: the single payment line id
|
|
:param amount: the (signed) amount debited from the bank account
|
|
:param currency: the bank account's currency *browse object*
|
|
:param boolean storno_retry: when True, attempt to reopen the invoice, \
|
|
set the invoice to 'Debit denied' otherwise.
|
|
:return: an incomplete reconcile for the caller to fill
|
|
:rtype: database id of an account.move.reconcile resource.
|
|
"""
|
|
|
|
move_line_obj = self.pool.get('account.move.line')
|
|
reconcile_obj = self.pool.get('account.move.reconcile')
|
|
line = self.browse(cr, uid, payment_line_id)
|
|
reconcile_id = False
|
|
if (line.debit_move_line_id and not line.storno and
|
|
self.pool.get('res.currency').is_zero(
|
|
cr, uid, currency, (
|
|
(line.debit_move_line_id.credit or 0.0) -
|
|
(line.debit_move_line_id.debit or 0.0) + amount))):
|
|
# Two different cases, full and partial
|
|
# Both cases differ subtly in the procedure to follow
|
|
# Needs refractoring, but why is this not in the OpenERP API?
|
|
# Actually, given the nature of a direct debit order and storno,
|
|
# we should not need to take partial into account on the side of
|
|
# the debit_move_line.
|
|
if line.debit_move_line_id.reconcile_partial_id:
|
|
reconcile_id = line.debit_move_line_id.reconcile_partial_id.id
|
|
attribute = 'reconcile_partial_id'
|
|
if len(line.debit_move_line_id.reconcile_id.line_partial_ids) == 2:
|
|
# reuse the simple reconcile for the storno transfer
|
|
reconcile_obj.write(
|
|
cr, uid, reconcile_id, {
|
|
'line_id': [(6, 0, line.debit_move_line_id.id)],
|
|
'line_partial_ids': [(6, 0, [])],
|
|
}, context=context)
|
|
else:
|
|
# split up the original reconcile in a partial one
|
|
# and a new one for reconciling the storno transfer
|
|
reconcile_obj.write(
|
|
cr, uid, reconcile_id, {
|
|
'line_partial_ids': [(3, line.debit_move_line_id.id)],
|
|
}, context=context)
|
|
reconcile_id = reconcile_obj.create(
|
|
cr, uid, {
|
|
'type': 'auto',
|
|
'line_id': [(6, 0, line.debit_move_line_id.id)],
|
|
}, context=context)
|
|
elif line.debit_move_line_id.reconcile_id:
|
|
reconcile_id = line.debit_move_line_id.reconcile_id.id
|
|
if len(line.debit_move_line_id.reconcile_id.line_id) == 2:
|
|
# reuse the simple reconcile for the storno transfer
|
|
reconcile_obj.write(
|
|
cr, uid, reconcile_id, {
|
|
'line_id': [(6, 0, [line.debit_move_line_id.id])]
|
|
}, context=context)
|
|
else:
|
|
# split up the original reconcile in a partial one
|
|
# and a new one for reconciling the storno transfer
|
|
partial_ids = [
|
|
x.id for x in line.debit_move_line_id.reconcile_id.line_id
|
|
if x.id != line.debit_move_line_id.id
|
|
]
|
|
reconcile_obj.write(
|
|
cr, uid, reconcile_id, {
|
|
'line_partial_ids': [(6, 0, partial_ids)],
|
|
'line_id': [(6, 0, [])],
|
|
}, context=context)
|
|
reconcile_id = reconcile_obj.create(
|
|
cr, uid, {
|
|
'type': 'auto',
|
|
'line_id': [(6, 0, line.debit_move_line_id.id)],
|
|
}, context=context)
|
|
# mark the payment line for storno processed
|
|
if reconcile_id:
|
|
self.write(cr, uid, [payment_line_id],
|
|
{'storno': True}, context=context)
|
|
# put forth the invoice workflow
|
|
if line.move_line_id.invoice:
|
|
activity = (storno_retry and 'open_test'
|
|
or 'invoice_debit_denied')
|
|
netsvc.LocalService("workflow").trg_validate(
|
|
uid, 'account.invoice', line.move_line_id.invoice.id,
|
|
activity, cr)
|
|
return reconcile_id
|
|
|
|
def get_storno_account_id(self, cr, uid, payment_line_id, amount,
|
|
currency, context=None):
|
|
"""
|
|
Check the match of the arguments, and return the account associated
|
|
with the storno.
|
|
Used in account_banking interactive mode
|
|
|
|
:param payment_line_id: the single payment line id
|
|
:param amount: the (signed) amount debited from the bank account
|
|
:param currency: the bank account's currency *browse object*
|
|
:return: an account if there is a full match, False otherwise
|
|
:rtype: database id of an account.account resource.
|
|
"""
|
|
|
|
line = self.browse(cr, uid, payment_line_id)
|
|
account_id = False
|
|
if (line.debit_move_line_id and not line.storno and
|
|
self.pool.get('res.currency').is_zero(
|
|
cr, uid, currency, (
|
|
(line.debit_move_line_id.credit or 0.0) -
|
|
(line.debit_move_line_id.debit or 0.0) + amount))):
|
|
account_id = line.debit_move_line_id.account_id.id
|
|
return account_id
|
|
|
|
def debit_reconcile(self, cr, uid, payment_line_id, context=None):
|
|
"""
|
|
Reconcile a debit order's payment line with the the move line
|
|
that it is based on.
|
|
As the amount is derived directly from the counterpart move line,
|
|
we do not expect a write off. Take partially reconcilions into
|
|
account though.
|
|
|
|
:param payment_line_id: the single id of the canceled payment line
|
|
"""
|
|
|
|
if isinstance(payment_line_id, (list, tuple)):
|
|
payment_line_id = payment_line_id[0]
|
|
reconcile_obj = self.pool.get('account.move.reconcile')
|
|
move_line_obj = self.pool.get('account.move.line')
|
|
payment_line = self.browse(cr, uid, payment_line_id, context=context)
|
|
|
|
debit_move_line = payment_line.debit_move_line_id
|
|
torec_move_line = payment_line.move_line_id
|
|
|
|
if payment_line.storno:
|
|
raise osv.except_osv(
|
|
_('Can not reconcile'),
|
|
_('Cancelation of payment line \'%s\' has already been ' +
|
|
'processed') % payment_line.name)
|
|
if (not debit_move_line or not torec_move_line):
|
|
raise osv.except_osv(
|
|
_('Can not reconcile'),
|
|
_('No move line for line %s') % payment_line.name)
|
|
if torec_move_line.reconcile_id: # torec_move_line.reconcile_partial_id:
|
|
raise osv.except_osv(
|
|
_('Error'),
|
|
_('Move line %s has already been reconciled') %
|
|
torec_move_line.name
|
|
)
|
|
if debit_move_line.reconcile_id or debit_move_line.reconcile_partial_id:
|
|
raise osv.except_osv(
|
|
_('Error'),
|
|
_('Move line %s has already been reconciled') %
|
|
debit_move_line.name
|
|
)
|
|
|
|
def is_zero(total):
|
|
return self.pool.get('res.currency').is_zero(
|
|
cr, uid, debit_move_line.company_id.currency_id, total)
|
|
|
|
line_ids = [debit_move_line.id, torec_move_line.id]
|
|
if torec_move_line.reconcile_partial_id:
|
|
line_ids = [
|
|
x.id for x in debit_move_line.reconcile_partial_id.line_partial_ids] + [torec_move_line_id]
|
|
|
|
total = move_line_obj.get_balance(cr, uid, line_ids)
|
|
vals = {
|
|
'type': 'auto',
|
|
'line_id': is_zero(total) and [(6, 0, line_ids)] or [(6, 0, [])],
|
|
'line_partial_ids': is_zero(total) and [(6, 0, [])] or [(6, 0, line_ids)],
|
|
}
|
|
|
|
if torec_move_line.reconcile_partial_id:
|
|
reconcile_obj.write(
|
|
cr, uid, debit_move_line.reconcile_partial_id.id,
|
|
vals, context=context)
|
|
else:
|
|
reconcile_obj.create(
|
|
cr, uid, vals, context=context)
|
|
for line_id in line_ids:
|
|
netsvc.LocalService("workflow").trg_trigger(
|
|
uid, 'account.move.line', line_id, cr)
|
|
|
|
# If a bank transaction of a storno was first confirmed
|
|
# and now canceled (the invoice is now in state 'debit_denied'
|
|
if torec_move_line.invoice:
|
|
netsvc.LocalService("workflow").trg_validate(
|
|
uid, 'account.invoice', torec_move_line.invoice.id,
|
|
'undo_debit_denied', cr)
|
|
|
|
|
|
|
|
_columns = {
|
|
'debit_move_line_id': fields.many2one(
|
|
# this line is part of the credit side of move 2a
|
|
# from the documentation
|
|
'account.move.line', 'Debit move line',
|
|
readonly=True,
|
|
help="Move line through which the debit order pays the invoice"),
|
|
'storno': fields.boolean(
|
|
'Storno',
|
|
readonly=True,
|
|
help=("If this is true, the debit order has been canceled " +
|
|
"by the bank or by the customer")),
|
|
}
|
|
payment_line()
|
|
|
|
|
|
class payment_order_create(osv.osv_memory):
|
|
_inherit = 'payment.order.create'
|
|
|
|
def search_entries(self, cr, uid, ids, context=None):
|
|
"""
|
|
This method taken from account_payment module.
|
|
We adapt the domain based on the payment_order_type
|
|
"""
|
|
line_obj = self.pool.get('account.move.line')
|
|
mod_obj = self.pool.get('ir.model.data')
|
|
if context is None:
|
|
context = {}
|
|
data = self.read(cr, uid, ids, [], context=context)[0]
|
|
search_due_date = data['duedate']
|
|
|
|
### start account_direct_debit ###
|
|
payment = self.pool.get('payment.order').browse(cr, uid, context['active_id'], context=context)
|
|
# Search for move line to pay:
|
|
if payment.payment_order_type == 'debit':
|
|
domain = [
|
|
('reconcile_id', '=', False),
|
|
('account_id.type', '=', 'receivable'),
|
|
('amount_to_receive', '>', 0),
|
|
]
|
|
# cannot filter on properties of (searchable)
|
|
# function fields. Needs work in expression.expression.parse()
|
|
# Currently gives an SQL error.
|
|
# apply payment term filter
|
|
if payment.mode.payment_term_ids:
|
|
term_ids = [term.id for term in payment.mode.payment_term_ids]
|
|
domain = domain + [
|
|
'|', ('invoice', '=', False),
|
|
('payment_term_id', 'in', term_ids),
|
|
]
|
|
else:
|
|
domain = [
|
|
('reconcile_id', '=', False),
|
|
('account_id.type', '=', 'payable'),
|
|
('amount_to_pay', '>', 0)
|
|
]
|
|
# domain = [('reconcile_id', '=', False), ('account_id.type', '=', 'payable'), ('amount_to_pay', '>', 0)]
|
|
### end account_direct_debit ###
|
|
|
|
domain = domain + ['|', ('date_maturity', '<=', search_due_date), ('date_maturity', '=', False)]
|
|
line_ids = line_obj.search(cr, uid, domain, context=context)
|
|
context.update({'line_ids': line_ids})
|
|
model_data_ids = mod_obj.search(cr, uid,[('model', '=', 'ir.ui.view'), ('name', '=', 'view_create_payment_order_lines')], context=context)
|
|
resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id']
|
|
return {'name': ('Entrie Lines'),
|
|
'context': context,
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'payment.order.create',
|
|
'views': [(resource_id,'form')],
|
|
'type': 'ir.actions.act_window',
|
|
'target': 'new',
|
|
}
|
|
payment_order_create()
|
|
|
|
|
|
|