mirror of
https://github.com/OCA/bank-payment.git
synced 2025-02-02 10:37:31 +02:00
[ADD] Invoice workflow: direct debit order payments
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
import account_payment
|
||||
import account_move_line
|
||||
import account_invoice
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
'depends': ['account_banking'],
|
||||
'init_xml': [],
|
||||
'update_xml': [
|
||||
'account_payment_view.xml',
|
||||
'view/account_payment.xml',
|
||||
'view/account_invoice.xml',
|
||||
'workflow/account_invoice.xml',
|
||||
],
|
||||
'demo_xml': [],
|
||||
'description': '''
|
||||
|
||||
126
account_direct_debit/account_invoice.py
Normal file
126
account_direct_debit/account_invoice.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from osv import osv, fields
|
||||
from tools.translate import _
|
||||
|
||||
"""
|
||||
Invoice workflow:
|
||||
|
||||
1 the sale leads to
|
||||
1300 Debtors 100
|
||||
8000 Sales 100
|
||||
|
||||
Balance:
|
||||
Debtors 2000 |
|
||||
Sales | 2000
|
||||
|
||||
2 an external booking takes place
|
||||
1100 Bank 100
|
||||
1300 Debtors 100
|
||||
This booking is reconciled with [1]
|
||||
The invoice gets set to state 'paid', and 'reconciled' = True
|
||||
|
||||
Balance:
|
||||
Debtors 1900 |
|
||||
Bank 100 |
|
||||
Sales | 2000
|
||||
|
||||
This module considers the following diversion:
|
||||
|
||||
2a the invoice is included in a direct debit order. When the order is confirmed:
|
||||
2000 Transfer account 100
|
||||
1300 Debtors 100
|
||||
Reconciliation takes place between 1 and 2a
|
||||
The invoice gets set to state 'paid', and 'reconciled' = True
|
||||
|
||||
Balance:
|
||||
Debtors 0 |
|
||||
Transfer account 2000 |
|
||||
Bank 0 |
|
||||
Sales | 2000
|
||||
|
||||
3a the direct debit order is booked on the bank account
|
||||
|
||||
Balance:
|
||||
1100 Bank 2000 |
|
||||
2000 Transfer account | 2000
|
||||
Reconciliation takes place between 3a and 2a
|
||||
|
||||
Balance:
|
||||
Debtors 0 |
|
||||
Transfer account 0 |
|
||||
Bank 2000 |
|
||||
Sales | 2000
|
||||
|
||||
4 a storno from invoice [1] triggers a new booking on the bank account
|
||||
1300 Debtors 100 |
|
||||
1100 Bank | 100
|
||||
|
||||
Balance:
|
||||
Debtors 100 |
|
||||
Transfer account 0 |
|
||||
Bank 1900 |
|
||||
Sales | 2000
|
||||
|
||||
The reconciliation of 2a is undone. The booking of 2a is reconciled
|
||||
with the booking of 4 instead (is this problematic?).
|
||||
The payment line attribute 'storno' is set to True
|
||||
|
||||
Two cases need to be distinguisted:
|
||||
1) If the storno is a manual storno from the partner, the invoice is set to
|
||||
state 'debit_denied',
|
||||
with 'reconciled' = False
|
||||
This module implements this option by allowing the bank module to call
|
||||
|
||||
netsvc.LocalService("workflow").trg_validate(
|
||||
uid, 'account.invoice', ids, 'debit_denied', cr)
|
||||
|
||||
2) If the storno is an error generated by the bank (assumingly non-fatal),
|
||||
the invoice is reopened for the next debit run. This is a call to existing
|
||||
|
||||
netsvc.LocalService("workflow").trg_validate(
|
||||
uid, 'account.invoice', ids, 'open_test', cr)
|
||||
|
||||
Should also be adding a log entry on the invoice for tracing purposes
|
||||
|
||||
self._log_event(cr, uid, ids, -1.0, 'Debit denied')
|
||||
|
||||
If not for that funny comment
|
||||
"#TODO: implement messages system" in account/invoice.py
|
||||
|
||||
Actual fatal errors need to be dealt with manually by checking open invoices
|
||||
by invoice- or due date.
|
||||
"""
|
||||
|
||||
class account_invoice(osv.osv):
|
||||
_inherit = "account.invoice"
|
||||
|
||||
def __init__(self, pool, cr):
|
||||
"""
|
||||
Adding a state to the hardcoded state list of the inherited
|
||||
model. The alternative is duplicating the field definition
|
||||
in columns but only one module can do that!
|
||||
|
||||
Maybe apply a similar trick when overriding the buttons' 'states' attributes
|
||||
in the form view, manipulating the xml in fields_view_get().
|
||||
"""
|
||||
super(account_invoice, self).__init__(pool, cr)
|
||||
invoice_obj = pool.get('account.invoice')
|
||||
invoice_obj._columns['state'].selection.append(
|
||||
('debit_denied', 'Debit denied'))
|
||||
|
||||
def action_debit_denied(self, cr, uid, ids, context=None):
|
||||
for invoice_id in ids:
|
||||
if self.test_paid(cr, uid, [invoice_id], context):
|
||||
number = self.read(
|
||||
cr, uid, invoice_id, ['number'], context=context)['number']
|
||||
raise osv.except_osv(
|
||||
_('Error !'),
|
||||
_('You cannot set invoice \'%s\' to state \'debit denied\', ' +
|
||||
'as it is still reconciled.') % number)
|
||||
self.write(cr, uid, ids, {'state':'debit_denied'}, context=context)
|
||||
for inv_id, name in self.name_get(cr, uid, ids, context=context):
|
||||
message = _("Invoice '%s': direct debit is denied.") % name
|
||||
self.log(cr, uid, inv_id, message)
|
||||
return True
|
||||
|
||||
account_invoice()
|
||||
@@ -44,6 +44,7 @@ class account_move_line(osv.osv):
|
||||
INNER JOIN payment_order po
|
||||
ON (pl.order_id = po.id)
|
||||
WHERE move_line_id = ml.id
|
||||
AND pl.storno is false
|
||||
AND po.state != 'cancel') AS amount
|
||||
FROM account_move_line ml
|
||||
WHERE id IN %s""", (tuple(ids),))
|
||||
@@ -63,6 +64,7 @@ class account_move_line(osv.osv):
|
||||
FROM payment_line pl
|
||||
INNER JOIN payment_order po ON (pl.order_id = po.id)
|
||||
WHERE move_line_id = l.id
|
||||
AND pl.storno is false
|
||||
AND po.state != 'cancel'
|
||||
) %(operator)s %%s ''' % {'operator': x[1]}, args))
|
||||
sql_args = tuple(map(itemgetter(2), args))
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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')],
|
||||
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'),
|
||||
),
|
||||
}
|
||||
payment_mode()
|
||||
|
||||
class payment_order(osv.osv):
|
||||
_inherit = 'payment.order'
|
||||
@@ -29,6 +48,119 @@ class payment_order(osv.osv):
|
||||
# also update the domain
|
||||
res['fields']['mode']['domain'] = domain
|
||||
return res
|
||||
|
||||
def _reconcile_debit_order_move_line(self, cr, uid, origin_move_line_id,
|
||||
transfer_move_line_id, context=None):
|
||||
"""
|
||||
Reconcile the debit order's move lines at generation time.
|
||||
As the amount is derived directly from the counterpart move line,
|
||||
we do not expect a write off. Take partially reconcilions into
|
||||
account though.
|
||||
"""
|
||||
reconcile_obj = self.pool.get('account.move.reconcile')
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
line_ids = [origin_move_line_id, transfer_move_line_id]
|
||||
(origin, transfer) = move_line_obj.browse(
|
||||
cr, uid, line_ids, context=context)
|
||||
if origin.reconcile_partial_id:
|
||||
line_ids = [x.id for x in
|
||||
origin.reconcile_partial_id.line_partial_ids + [transfer]
|
||||
]
|
||||
|
||||
total = 0.0
|
||||
company_currency_id = origin.company_id.currency_id
|
||||
for line in move_line_obj.read(
|
||||
cr, uid, line_ids, ['debit', 'credit'], context=context):
|
||||
total += (line['debit'] or 0.0) - (line['credit'] or 0.0)
|
||||
full = self.pool.get('res.currency').is_zero(
|
||||
cr, uid, company_currency_id, total)
|
||||
vals = {
|
||||
'type': 'auto',
|
||||
'line_id': full and [(6, 0, line_ids)] or [(6, 0, [])],
|
||||
'line_partial_ids': full and [(6, 0, [])] or [(6, 0, line_ids)],
|
||||
}
|
||||
if origin.reconcile_partial_id:
|
||||
reconcile_obj.write(
|
||||
cr, uid, origin.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)
|
||||
|
||||
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,
|
||||
}
|
||||
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)
|
||||
|
||||
payment_line_obj.write(
|
||||
cr, uid, line.id,
|
||||
{'debit_move_line_id': reconcile_move_line_id},
|
||||
context=context)
|
||||
|
||||
account_move_obj.post(cr, uid, [move_id], context=context)
|
||||
self._reconcile_debit_order_move_line(
|
||||
cr, uid, line.move_line_id.id, reconcile_move_line_id,
|
||||
context=context)
|
||||
return res
|
||||
|
||||
payment_order()
|
||||
|
||||
class payment_order_create(osv.osv_memory):
|
||||
@@ -71,3 +203,20 @@ class payment_order_create(osv.osv_memory):
|
||||
'target': 'new',
|
||||
}
|
||||
payment_order_create()
|
||||
|
||||
class payment_line(osv.osv):
|
||||
_inherit = 'payment.line'
|
||||
_columns = {
|
||||
'debit_move_line_id': fields.many2one(
|
||||
'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()
|
||||
|
||||
|
||||
|
||||
35
account_direct_debit/view/account_invoice.xml
Normal file
35
account_direct_debit/view/account_invoice.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="invoice_form" model="ir.ui.view">
|
||||
<field name="name">account.invoice.form</field>
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="account.invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<!--
|
||||
Add new state 'debit_denied' to applicable buttons.
|
||||
Maybe apply trick in fields_view_get instead, for
|
||||
better compatibility with other modules?
|
||||
-->
|
||||
<button name="invoice_open" position="attributes">
|
||||
<attribute name="states">draft,proforma2,debit_denied</attribute>
|
||||
</button>
|
||||
<button string='Re-Open' position="attributes">
|
||||
<attribute name="states">paid,debit_denied</attribute>
|
||||
<!--
|
||||
unintentional fix of
|
||||
https://bugs.launchpad.net/openobject-addons/+bug/807543
|
||||
-->
|
||||
<attribute name="groups"/>
|
||||
</button>
|
||||
<button name="invoice_open" position="after">
|
||||
<button name="invoice_debit_denied" states="paid"
|
||||
string="Debit Denied" icon="gtk-cancel"/>
|
||||
</button>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -6,6 +6,7 @@
|
||||
<field name="domain">[('payment_order_type', '=', 'payment')]</field>
|
||||
<field name="context">{'search_payment_order_type': 'payment'}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_debit_order_tree" model="ir.actions.act_window">
|
||||
<field name="name">Direct Debit Orders</field>
|
||||
<field name="res_model">payment.order</field>
|
||||
@@ -18,19 +19,7 @@
|
||||
</record>
|
||||
|
||||
<menuitem action="action_debit_order_tree" id="menu_action_debit_order_form" parent="account_payment.menu_main_payment" sequence="4"/>
|
||||
<!-- not needed I hope
|
||||
<record id="view_create_payment_order" model="ir.ui.view">
|
||||
<field name="name">payment.order.create.form</field>
|
||||
<field name="model">payment.order.create</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="account_payment.view_create_payment_order"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='search_entries']" position="attributes">
|
||||
<attribute name="name">search_entries</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
-->
|
||||
|
||||
<record id="view_payment_order_form" model="ir.ui.view">
|
||||
<field name="name">payment.order.form</field>
|
||||
<field name="model">payment.order</field>
|
||||
@@ -59,5 +48,19 @@
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Add transfer account for debit type modes -->
|
||||
<record model="ir.ui.view" id="view_payment_mode_form">
|
||||
<field name="name">payment.mode.form add transfer account</field>
|
||||
<field name="model">payment.mode</field>
|
||||
<field name="inherit_id" ref="account_banking.view_payment_mode_form_inherit"/>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="type" position="after">
|
||||
<field name="transfer_account_id"/>
|
||||
<field name="transfer_journal_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
21
account_direct_debit/workflow/account_invoice.xml
Normal file
21
account_direct_debit/workflow/account_invoice.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="act_debit_denied" model="workflow.activity">
|
||||
<field name="wkf_id" ref="account.wkf"/>
|
||||
<field name="name">debit_denied</field>
|
||||
<field name="action">action_debit_denied()</field>
|
||||
<field name="kind">function</field>
|
||||
<!-- field name="signal_send">subflow.debit_denied</field -->
|
||||
</record>
|
||||
<record id="paid_to_debit_denied" model="workflow.transition">
|
||||
<field name="act_from" ref="account.act_paid"/>
|
||||
<field name="act_to" ref="act_debit_denied"/>
|
||||
<field name="signal">invoice_debit_denied</field>
|
||||
</record>
|
||||
<record id="debit_denied_to_open" model="workflow.transition">
|
||||
<field name="act_from" ref="act_debit_denied"/>
|
||||
<field name="act_to" ref="account.act_open_test"/>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
Reference in New Issue
Block a user