[ADD] Invoice workflow: direct debit order payments

This commit is contained in:
Stefan Rijnhart
2011-12-10 23:03:59 +01:00
parent 8a6ffdf604
commit 5d287eefa3
8 changed files with 353 additions and 14 deletions

View File

@@ -1,2 +1,3 @@
import account_payment
import account_move_line
import account_invoice

View File

@@ -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': '''

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

View File

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

View File

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

View 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>

View File

@@ -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>

View 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>