Files
bank-payment/account_direct_debit/model/account_payment.py
OpenERP instance user db48a96e72 [FIX] show linked invoice on draft statement lines
[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
2011-12-18 19:56:54 +01:00

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()