From 810a7d795b9d63ba092d41a11ce25caef89bc701 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 8 Apr 2015 18:28:32 +0200 Subject: [PATCH] Add module account_banking_payment_transfer_simple --- .../__init__.py | 1 + .../__openerp__.py | 47 ++++ .../model/__init__.py | 4 + .../model/account_move_line.py | 45 ++++ .../model/account_payment.py | 246 ++++++++++++++++++ .../model/payment_line.py | 195 ++++++++++++++ .../model/payment_mode.py | 52 ++++ .../view/payment_mode.xml | 34 +++ .../workflow/account_payment_workflow.xml | 10 + 9 files changed, 634 insertions(+) create mode 100644 account_banking_payment_transfer_simple/__init__.py create mode 100644 account_banking_payment_transfer_simple/__openerp__.py create mode 100644 account_banking_payment_transfer_simple/model/__init__.py create mode 100644 account_banking_payment_transfer_simple/model/account_move_line.py create mode 100644 account_banking_payment_transfer_simple/model/account_payment.py create mode 100644 account_banking_payment_transfer_simple/model/payment_line.py create mode 100644 account_banking_payment_transfer_simple/model/payment_mode.py create mode 100644 account_banking_payment_transfer_simple/view/payment_mode.xml create mode 100644 account_banking_payment_transfer_simple/workflow/account_payment_workflow.xml diff --git a/account_banking_payment_transfer_simple/__init__.py b/account_banking_payment_transfer_simple/__init__.py new file mode 100644 index 0000000..9186ee3 --- /dev/null +++ b/account_banking_payment_transfer_simple/__init__.py @@ -0,0 +1 @@ +from . import model diff --git a/account_banking_payment_transfer_simple/__openerp__.py b/account_banking_payment_transfer_simple/__openerp__.py new file mode 100644 index 0000000..86ee4ff --- /dev/null +++ b/account_banking_payment_transfer_simple/__openerp__.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2009 EduSense BV (). +# (C) 2011 - 2013 Therp BV (). +# (C) 2014 ACSONE SA/NV (). +# +# All other contributions are (C) by their respective contributors +# +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name': 'Account Banking - Payments Transfer Account (simple)', + 'version': '0.2', + 'license': 'AGPL-3', + 'author': "Akretion,Odoo Community Association (OCA)", + 'website': 'http://www.akretion.com', + 'category': 'Banking addons', + 'depends': [ + 'account_banking_payment_export', + ], + 'data': [ + 'view/payment_mode.xml', + 'workflow/account_payment_workflow.xml', + ], + 'description': ''' +This is a simple equivalent for Odoo v7 of the module *account_banking_payment_transfer* for v8.0. + +I developped this module to be able to make some SEPA direct debits / credit transfer with transfer move on Odoo v7, without installing the module account_banking (I can't install account_banking because I used the OCA modules from https://github.com/OCA/bank-statement-reconcile) + ''', + 'installable': True, +} diff --git a/account_banking_payment_transfer_simple/model/__init__.py b/account_banking_payment_transfer_simple/model/__init__.py new file mode 100644 index 0000000..404e9c2 --- /dev/null +++ b/account_banking_payment_transfer_simple/model/__init__.py @@ -0,0 +1,4 @@ +from . import account_payment +from . import payment_line +from . import payment_mode +from . import account_move_line diff --git a/account_banking_payment_transfer_simple/model/account_move_line.py b/account_banking_payment_transfer_simple/model/account_move_line.py new file mode 100644 index 0000000..0cffd92 --- /dev/null +++ b/account_banking_payment_transfer_simple/model/account_move_line.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2009 EduSense BV (). +# (C) 2011 - 2013 Therp BV (). +# +# All other contributions are (C) by their respective contributors +# +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import orm + + +class account_move_line(orm.Model): + _inherit = "account.move.line" + + def get_balance(self, cr, uid, ids, context=None): + """ + Return the balance of any set of move lines. + + Not to be confused with the 'balance' field on this model, which + returns the account balance that the move line applies to. + """ + total = 0.0 + if not ids: + return total + for line in self.read( + cr, uid, ids, ['debit', 'credit'], context=context): + total += (line['debit'] or 0.0) - (line['credit'] or 0.0) + return total diff --git a/account_banking_payment_transfer_simple/model/account_payment.py b/account_banking_payment_transfer_simple/model/account_payment.py new file mode 100644 index 0000000..096ccff --- /dev/null +++ b/account_banking_payment_transfer_simple/model/account_payment.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2009 EduSense BV (). +# (C) 2011 - 2013 Therp BV (). +# (C) 2014 ACSONE SA (). +# (C) 2014 Akretion (www.akretion.com) +# +# All other contributions are (C) by their respective contributors +# +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import orm, fields +from openerp.tools.translate import _ + + +class PaymentOrder(orm.Model): + _inherit = 'payment.order' + +# @api.multi +# def get_partial_reconcile_ids(self): +# self.ensure_one() +# reconcile_partial_ids = [line.move_line_id.reconcile_partial_id.id +# for line in self.line_ids if +# line.move_line_id.reconcile_partial_id.id] +# return reconcile_partial_ids + +# @api.one +# def get_partial_reconcile_count(self): +# self.partial_reconcile_count = len(self.get_partial_reconcile_ids()) + + def action_rejected(self, cr, uid, ids, context=None): + return True + +# @api.multi +# def action_done(self): +# for line in self.line_ids: +# line.date_done = fields.Date.context_today(self) +# self.date_done = fields.Date.context_today(self) + # state is written in workflow definition +# return True + +# @api.multi +# def _get_transfer_move_lines(self): +# """ +# Get the transfer move lines (on the transfer account). +# """ +# res = [] +# for order in self: +# for order_line in order.line_ids: +# move_line = order_line.transfer_move_line_id +# if move_line: +# res.append(move_line) +# return res + +# @api.multi +# def get_transfer_move_line_ids(self, *args): +# '''Used in the workflow for trigger_expr_id''' +# return [move_line.id for move_line in self._get_transfer_move_lines()] + +# @api.multi +# def test_done(self): +# """ +# Test if all moves on the transfer account are reconciled. +# +# Called from the workflow to move to the done state when +# all transfer move have been reconciled through bank statements. +# """ +# return all([move_line.reconcile_id for move_line in +# self._get_transfer_move_lines()]) + +# @api.multi +# def test_undo_done(self): +# return not self.test_done() + + def _prepare_transfer_move(self, cr, uid, order, context=None): + # TODO question : can I use self.mode.xxx in an @api.model ?? + # It works, but I'm not sure we are supposed to do that ! + # I didn't want to use api.one to avoid having to + # do self._prepare_transfer_move()[0] in action_sent + # I prefer to just have to do self._prepare_transfer_move() + vals = { + 'journal_id': order.mode.transfer_journal_id.id, + 'ref': '%s %s' % ( + order.payment_order_type[:3].upper(), order.reference) + } + return vals + + def _prepare_move_line_transfer_account( + self, cr, uid, order, amount, move_id, payment_lines, labels, + context=None): + payment_order_type = order.payment_order_type + if len(payment_lines) == 1: + partner_id = payment_lines[0].partner_id.id + name = _('%s line %s') % ( + labels[payment_order_type], payment_lines[0].name) + else: + partner_id = False + name = '%s %s' % ( + labels[payment_order_type], order.reference) + vals = { + 'name': name, + 'move_id': move_id, + 'partner_id': partner_id, + 'account_id': order.mode.transfer_account_id.id, + 'credit': (payment_order_type == 'payment' and + amount or 0.0), + 'debit': (payment_order_type == 'debit' and + amount or 0.0), + } + return vals + + def _prepare_move_line_partner_account( + self, cr, uid, line, move_id, labels, context=None): + payment_order_type = line.order_id.payment_order_type + if line.move_line_id: + account_id = line.move_line_id.account_id.id + else: + if payment_order_type == 'debit': + account_id = line.partner_id.property_account_receivable.id + else: + account_id = line.partner_id.property_account_payable.id + vals = { + 'name': _('%s line %s') % ( + labels[payment_order_type], line.name), + 'move_id': move_id, + 'partner_id': line.partner_id.id, + 'account_id': account_id, + 'credit': (payment_order_type == 'debit' and + line.amount or 0.0), + 'debit': (payment_order_type == 'payment' and + line.amount or 0.0), + } + return vals + +# @api.model +# def action_sent_no_move_line_hook(self, pay_line): +# """This function is designed to be inherited""" +# return + + def action_done(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. + """ + am_obj = self.pool['account.move'] + aml_obj = self.pool['account.move.line'] + pl_obj = self.pool['payment.line'] + labels = { + 'payment': _('Payment order'), + 'debit': _('Direct debit order'), + } + for order in self.browse(cr, uid, ids, context=context): + if order.mode.transfer_journal_id and order.mode.transfer_account_id: + # prepare a dict "trfmoves" that can be used when + # self.mode.transfer_move_option = date or line + # key = unique identifier (date or True or line.id) + # value = [pay_line1, pay_line2, ...] + trfmoves = {} + if order.mode.transfer_move_option == 'line': + for line in order.line_ids: + trfmoves[line.id] = [line] + else: + if order.date_prefered in ('now', 'fixed'): + trfmoves[True] = [] + for line in order.line_ids: + trfmoves[True].append(line) + else: # date_prefered == due + for line in order.line_ids: + if line.date in trfmoves: + trfmoves[line.date].append(line) + else: + trfmoves[line.date] = [line] + + for identifier, lines in trfmoves.iteritems(): + mvals = self._prepare_transfer_move( + cr, uid, order, context=context) + move_id = am_obj.create(cr, uid, mvals, context=context) + total_amount = 0 + for line in lines: + # TODO: take multicurrency into account + + # create the payment/debit counterpart move line + # on the partner account + partner_ml_vals = self._prepare_move_line_partner_account( + cr, uid, line, move_id, labels, context=context) + partner_move_line_id = aml_obj.create( + cr, uid, partner_ml_vals, context=context) + total_amount += line.amount + + # register the payment/debit move line + # on the payment line and call reconciliation on it + line.write({'transit_move_line_id': partner_move_line_id}) + + if line.move_line_id: + pl_obj.debit_reconcile(cr, uid, line.id, context=context) + #else: + # self.action_sent_no_move_line_hook(line) + + # create the payment/debit move line on the transfer account + trf_ml_vals = self._prepare_move_line_transfer_account( + cr, uid, order, total_amount, move_id, lines, labels, + context=context) + aml_obj.create(cr, uid, trf_ml_vals, context=context) + + # post account move + am_obj.post(cr, uid, [move_id], context=context) + + # State field is written by act_sent_wait + order.write({'state': 'done'}) + return True + +# @api.multi +# def partial(self): +# self.ensure_one() +# view_id = self.env.ref('account.view_move_line_tree').id +# reconcile_partial_ids = self.get_partial_reconcile_ids() +# reconcile_partial_domain = [('reconcile_partial_id', 'in', +# reconcile_partial_ids)] +# return { +# 'name': _('Partial Reconcile Moves Line'), +# 'context': self.env.context, +# 'domain': reconcile_partial_domain, +# 'view_type': 'form', +# 'view_mode': 'tree,form', +# 'res_model': 'account.move.line', +# 'views': [(view_id, 'tree')], +# 'type': 'ir.actions.act_window', +# 'target': 'current', +# } diff --git a/account_banking_payment_transfer_simple/model/payment_line.py b/account_banking_payment_transfer_simple/model/payment_line.py new file mode 100644 index 0000000..200a0d4 --- /dev/null +++ b/account_banking_payment_transfer_simple/model/payment_line.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2009 EduSense BV (). +# (C) 2011 - 2013 Therp BV (). +# +# All other contributions are (C) by their respective contributors +# +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import orm, fields +from openerp import netsvc +from openerp.tools.translate import _ + + +class PaymentLine(orm.Model): + ''' + Add some fields; make destination bank account + mandatory, as it makes no sense to send payments into thin air. + Edit: Payments can be by cash too, which is prohibited by mandatory bank + accounts. + ''' + _inherit = 'payment.line' + + def _get_transfer_move_line(self, cr, uid, ids, name, arg, context=None): + res = {} + for order_line in self.browse(cr, uid, ids, context=context): + if order_line.transit_move_line_id: + order_type = order_line.order_id.payment_order_type + trf_lines = order_line.transit_move_line_id.move_id.line_id + for move_line in trf_lines: + if order_type == 'debit' and move_line.debit > 0: + res[order_line.id] = move_line.id + elif order_type == 'payment' and move_line.credit > 0: + res[order_line.id] = move_line.id + else: + res[order_line.id] = False + return res + + _columns = { + 'msg': fields.char('Message', size=255, required=False, readonly=True), + 'date_done': fields.date( + 'Date Confirmed', select=True, readonly=True), + 'transit_move_line_id': fields.many2one( + 'account.move.line', 'Transfer move line', + readonly=True, + help="Move line through which the payment/debit order " + "pays the invoice", + ), + 'transfer_move_line_id': fields.function( + _get_transfer_move_line, + type='many2one', + relation='account.move.line', + string='Transfer move line counterpart', + readonly=True, + help="Counterpart move line on the transfer account", + ), + } + + _defaults = { + 'msg': '', + } + + """ + Hooks for processing direct debit orders, such as implemented in + account_direct_debit module. + """ + def get_storno_account_id(self, cr, uid, payment_line_id, amount, + currency_id, context=None): + """ + Hook for verifying a match of the payment line with the amount. + 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. + """ + + return False + + def debit_storno(self, cr, uid, payment_line_id, amount, + currency_id, storno_retry=True, context=None): + """ + Hook for handling a canceled item of a direct debit order. + Presumably called from a bank statement import routine. + + Decide on the direction that the invoice's workflow needs to take. + You may optionally return an incomplete reconcile for the caller + to reconcile the now void payment. + + :param payment_line_id: the single payment line id + :param amount: the (negative) amount debited from the bank account + :param currency: the bank account's currency *browse object* + :param boolean storno_retry: whether the storno is considered fatal \ + or not. + :return: an incomplete reconcile for the caller to fill + :rtype: database id of an account.move.reconcile resource. + """ + + return False + + 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. Called from payment_order.action_sent(). + As the amount is derived directly from the counterpart move line, + we do not expect a write off. Take partial reconciliations 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) + + transit_move_line = payment_line.transit_move_line_id + torec_move_line = payment_line.move_line_id + + if (not transit_move_line or not torec_move_line): + raise orm.except_orm( + _('Can not reconcile'), + _('No move line for line %s') % payment_line.name + ) + if torec_move_line.reconcile_id: + raise orm.except_orm( + _('Error'), + _('Move line %s has already been reconciled') % + torec_move_line.name + ) + if (transit_move_line.reconcile_id or + transit_move_line.reconcile_partial_id): + raise orm.except_orm( + _('Error'), + _('Move line %s has already been reconciled') % + transit_move_line.name + ) + + def is_zero(total): + return self.pool.get('res.currency').is_zero( + cr, uid, transit_move_line.company_id.currency_id, total) + + line_ids = [transit_move_line.id, torec_move_line.id] + if torec_move_line.reconcile_partial_id: + line_ids = [ + x.id for x in + torec_move_line.reconcile_partial_id.line_partial_ids + ] + [transit_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, [torec_move_line.reconcile_partial_id.id], + vals, context=context) + else: + reconcile_obj.create( + cr, uid, vals, context=context) + workflow = netsvc.LocalService("workflow") + for line_id in line_ids: + 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: + workflow.trg_validate( + uid, 'account.invoice', torec_move_line.invoice.id, + 'undo_debit_denied', cr) diff --git a/account_banking_payment_transfer_simple/model/payment_mode.py b/account_banking_payment_transfer_simple/model/payment_mode.py new file mode 100644 index 0000000..5030635 --- /dev/null +++ b/account_banking_payment_transfer_simple/model/payment_mode.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2009 EduSense BV (). +# (C) 2011 - 2013 Therp BV (). +# (C) 2014 Akretion (www.akretion.com) +# +# All other contributions are (C) by their respective contributors +# +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import orm, fields + + +class PaymentMode(orm.Model): + _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. You can only select accounts of type regular ' + 'that are marked for reconciliation'), + 'transfer_journal_id': fields.many2one( + 'account.journal', 'Transfer journal', + help='Journal to write payment entries when confirming ' + 'a debit order of this mode'), + 'transfer_move_option': fields.selection([ + ('date', 'One move per payment date'), + ('line', 'One move per payment line'), + ], 'Transfer move option') + } + + _defaults = { + 'transfer_move_option': 'date', + } diff --git a/account_banking_payment_transfer_simple/view/payment_mode.xml b/account_banking_payment_transfer_simple/view/payment_mode.xml new file mode 100644 index 0000000..63cafb4 --- /dev/null +++ b/account_banking_payment_transfer_simple/view/payment_mode.xml @@ -0,0 +1,34 @@ + + + + + + + payment.mode.form.inherit + payment.mode + + + + + + + + + + + + + + diff --git a/account_banking_payment_transfer_simple/workflow/account_payment_workflow.xml b/account_banking_payment_transfer_simple/workflow/account_payment_workflow.xml new file mode 100644 index 0000000..9e84557 --- /dev/null +++ b/account_banking_payment_transfer_simple/workflow/account_payment_workflow.xml @@ -0,0 +1,10 @@ + + + + + + action_done() + + + +