From 77a9e732f75d981f0d43462666b55e9fce4d9696 Mon Sep 17 00:00:00 2001
From: Alexis de Lattre
Date: Sat, 3 Aug 2013 01:12:36 +0200
Subject: [PATCH 01/43] Add module account_banking_sepa_direct_debit that
implements pain.008.001.02, pain.008.001.03 and pain.008.001.04. This module
is not ready yet : the management of mandates is still missing. I am
currently trying to get more information about these mandates to decide what
is the best implemtation of the data model of the mandates (O2M on
res.partner ? O2M os res.partner.bank ?).
---
account_banking_sepa_direct_debit/__init__.py | 26 +
.../__openerp__.py | 50 +
.../account_banking_sdd.py | 77 ++
.../account_banking_sdd_view.xml | 83 ++
account_banking_sepa_direct_debit/company.py | 75 ++
.../company_view.xml | 22 +
.../data/pain.008.001.02.xsd | 879 +++++++++++++++++
.../data/pain.008.001.03.xsd | 925 ++++++++++++++++++
.../data/pain.008.001.04.xsd | 892 +++++++++++++++++
.../data/payment_type_sdd.xml | 37 +
.../security/ir.model.access.csv | 2 +
.../wizard/__init__.py | 23 +
.../wizard/export_sdd.py | 391 ++++++++
.../wizard/export_sdd_view.xml | 41 +
14 files changed, 3523 insertions(+)
create mode 100644 account_banking_sepa_direct_debit/__init__.py
create mode 100644 account_banking_sepa_direct_debit/__openerp__.py
create mode 100644 account_banking_sepa_direct_debit/account_banking_sdd.py
create mode 100644 account_banking_sepa_direct_debit/account_banking_sdd_view.xml
create mode 100644 account_banking_sepa_direct_debit/company.py
create mode 100644 account_banking_sepa_direct_debit/company_view.xml
create mode 100644 account_banking_sepa_direct_debit/data/pain.008.001.02.xsd
create mode 100644 account_banking_sepa_direct_debit/data/pain.008.001.03.xsd
create mode 100644 account_banking_sepa_direct_debit/data/pain.008.001.04.xsd
create mode 100644 account_banking_sepa_direct_debit/data/payment_type_sdd.xml
create mode 100644 account_banking_sepa_direct_debit/security/ir.model.access.csv
create mode 100644 account_banking_sepa_direct_debit/wizard/__init__.py
create mode 100644 account_banking_sepa_direct_debit/wizard/export_sdd.py
create mode 100644 account_banking_sepa_direct_debit/wizard/export_sdd_view.xml
diff --git a/account_banking_sepa_direct_debit/__init__.py b/account_banking_sepa_direct_debit/__init__.py
new file mode 100644
index 000000000..bda7501b7
--- /dev/null
+++ b/account_banking_sepa_direct_debit/__init__.py
@@ -0,0 +1,26 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# SEPA Direct Debit module for OpenERP
+# Copyright (C) 2013 Akretion (http://www.akretion.com)
+# @author: Alexis de Lattre
+#
+# 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 .
+#
+##############################################################################
+
+import company
+import wizard
+import account_banking_sdd
+
diff --git a/account_banking_sepa_direct_debit/__openerp__.py b/account_banking_sepa_direct_debit/__openerp__.py
new file mode 100644
index 000000000..fc329a073
--- /dev/null
+++ b/account_banking_sepa_direct_debit/__openerp__.py
@@ -0,0 +1,50 @@
+##############################################################################
+#
+# SEPA Direct Debit module for OpenERP
+# Copyright (C) 2013 Akretion (http://www.akretion.com)
+# @author: Alexis de Lattre
+#
+# 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 SEPA Direct Debit',
+ 'summary': 'Create SEPA XML files for Direct Debit',
+ 'version': '0.1',
+ 'license': 'AGPL-3',
+ 'author': 'Akretion',
+ 'website': 'http://www.akretion.com',
+ 'category': 'Banking addons',
+ 'depends': ['account_direct_debit'],
+ 'data': [
+ 'account_banking_sdd_view.xml',
+ 'company_view.xml',
+ 'wizard/export_sdd_view.xml',
+ 'data/payment_type_sdd.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'description': '''
+Module to export direct debit payment orders in SEPA XML file format.
+
+SEPA PAIN (PAyment INitiation) is the new european standard for Customer-to-Bank payment instructions. This module implements SEPA Direct Debit (SDD), more specifically PAIN versions 008.001.02, 008.001.03 and 008.001.04. It is part of the ISO 20022 standard, available on http://www.iso20022.org.
+
+The Implementation Guidelines for SEPA Direct Debit published by the European Payments Council (http://http://www.europeanpaymentscouncil.eu) use PAIN version 008.001.02. So if you don't know which version your bank supports, you should try version 008.001.02 first.
+
+This module uses the framework provided by the banking addons, cf https://launchpad.net/banking-addons
+
+Please contact Alexis de Lattre from Akretion for any help or question about this module.
+ ''',
+ 'active': False,
+ 'installable': True,
+}
diff --git a/account_banking_sepa_direct_debit/account_banking_sdd.py b/account_banking_sepa_direct_debit/account_banking_sdd.py
new file mode 100644
index 000000000..aa08918a2
--- /dev/null
+++ b/account_banking_sepa_direct_debit/account_banking_sdd.py
@@ -0,0 +1,77 @@
+##############################################################################
+#
+# SEPA Direct Debit module for OpenERP
+# Copyright (C) 2013 Akretion (http://www.akretion.com)
+# @author: Alexis de Lattre
+#
+# 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
+import time
+from openerp.tools.translate import _
+from openerp.addons.decimal_precision import decimal_precision as dp
+
+
+class banking_export_sdd(orm.Model):
+ '''SEPA Direct Debit export'''
+ _name = 'banking.export.sdd'
+ _description = __doc__
+ _rec_name = 'msg_identification'
+
+ def _generate_filename(self, cr, uid, ids, name, arg, context=None):
+ res = {}
+ for sepa_file in self.browse(cr, uid, ids, context=context):
+ res[sepa_file.id] = 'sdd_' + (sepa_file.msg_identification or '') + '.xml'
+ return res
+
+ _columns = {
+ 'payment_order_ids': fields.many2many(
+ 'payment.order',
+ 'account_payment_order_sdd_rel',
+ 'banking_export_sepa_id', 'account_order_id',
+ 'Payment orders',
+ readonly=True),
+ 'requested_collec_date': fields.date('Requested collection date', readonly=True),
+ 'nb_transactions': fields.integer('Number of transactions', readonly=True),
+ 'total_amount': fields.float('Total amount',
+ digits_compute=dp.get_precision('Account'), readonly=True),
+ 'msg_identification': fields.char('Message identification', size=35,
+ readonly=True),
+ 'batch_booking': fields.boolean('Batch booking', readonly=True,
+ help="If true, the bank statement will display only one credit line for all the direct debits of the SEPA XML file ; if false, the bank statement will display one credit line per direct debit of the SEPA XML file."),
+ 'charge_bearer': fields.selection([
+ ('SHAR', 'Shared'),
+ ('CRED', 'Borne by creditor'),
+ ('DEBT', 'Borne by debtor'),
+ ('SLEV', 'Following service level'),
+ ], 'Charge bearer', readonly=True,
+ help='Shared : transaction charges on the sender side are to be borne by the debtor, transaction charges on the receiver side are to be borne by the creditor (most transfers use this). Borne by creditor : all transaction charges are to be borne by the creditor. Borne by debtor : all transaction charges are to be borne by the debtor. Following service level : transaction charges are to be applied following the rules agreed in the service level and/or scheme.'),
+ 'generation_date': fields.datetime('Generation date',
+ readonly=True),
+ 'file': fields.binary('SEPA XML file', readonly=True),
+ 'filename': fields.function(_generate_filename, type='char', size=256,
+ method=True, string='Filename', readonly=True),
+ 'state': fields.selection([
+ ('draft', 'Draft'),
+ ('sent', 'Sent'),
+ ('done', 'Reconciled'),
+ ], 'State', readonly=True),
+ }
+
+ _defaults = {
+ 'generation_date': fields.date.context_today,
+ 'state': 'draft',
+ }
diff --git a/account_banking_sepa_direct_debit/account_banking_sdd_view.xml b/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
new file mode 100644
index 000000000..6dcfb903f
--- /dev/null
+++ b/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+ account.banking.export.sdd.form
+ banking.export.sdd
+
+
+
+
+
+
+
+ account.banking.export.sdd.tree
+ banking.export.sdd
+
+
+
+
+
+
+
+
+
+
+
+
+ Generated SEPA Direct Debit XML files
+ banking.export.sdd
+ form
+ tree,form
+
+
+
+
+
+
+
+
+
diff --git a/account_banking_sepa_direct_debit/company.py b/account_banking_sepa_direct_debit/company.py
new file mode 100644
index 000000000..d85fc6fd7
--- /dev/null
+++ b/account_banking_sepa_direct_debit/company.py
@@ -0,0 +1,75 @@
+##############################################################################
+#
+# SEPA Direct Debit module for OpenERP
+# Copyright (C) 2013 Akretion (http://www.akretion.com)
+# @author: Alexis de Lattre
+#
+# 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
+import logging
+
+logger = logging.getLogger(__name__)
+
+class res_company(orm.Model):
+ _inherit = 'res.company'
+
+ _columns = {
+ 'sepa_creditor_identifier': fields.char('SEPA Creditor Identifier', size=35,
+ help="Enter the Creditor Identifier that has been attributed to your company to make SEPA Direct Debits. This identifier is composed of :\n- your country ISO code (2 letters)\n- a 2-digits checkum\n- a 3-letters business code\n- a country-specific identifier"),
+ }
+
+
+ def is_sepa_creditor_identifier_valid(self, cr, uid, sepa_creditor_identifier, context=None):
+ """Check if SEPA Creditor Identifier is valid
+ @param sepa_creditor_identifier: SEPA Creditor Identifier as str or unicode
+ @return: True if valid, False otherwise
+ """
+ if not isinstance(sepa_creditor_identifier, (str, unicode)):
+ return False
+ try:
+ sci_str = str(sepa_creditor_identifier)
+ except:
+ logger.warning("SEPA Creditor ID should contain only ASCII caracters.")
+ return False
+ sci = sci_str.lower()
+ if len(sci) < 9:
+ return False
+ before_replacement = sci[7:] + sci[0:2] + '00'
+ logger.debug("SEPA ID check before_replacement = %s" % before_replacement)
+ after_replacement = ''
+ for char in before_replacement:
+ if char.isalpha():
+ after_replacement += str(ord(char)-87)
+ else:
+ after_replacement += char
+ logger.debug("SEPA ID check after_replacement = %s" % after_replacement)
+ if int(sci[2:4]) == (98 - (int(after_replacement) % 97)):
+ return True
+ else:
+ return False
+
+
+ def _check_sepa_creditor_identifier(self, cr, uid, ids):
+ for company in self.browse(cr, uid, ids):
+ if company.sepa_creditor_identifier:
+ if not self.is_sepa_creditor_identifier_valid(cr, uid, company.sepa_creditor_identifier):
+ return False
+ return True
+
+ _constraints = [
+ (_check_sepa_creditor_identifier, "Invalid SEPA Creditor Identifier.", ['sepa_creditor_identifier']),
+ ]
diff --git a/account_banking_sepa_direct_debit/company_view.xml b/account_banking_sepa_direct_debit/company_view.xml
new file mode 100644
index 000000000..7691844c1
--- /dev/null
+++ b/account_banking_sepa_direct_debit/company_view.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ sepa_direct_debit.res.company.form
+ res.company
+
+
+
+
+
+
+
+
+
+
diff --git a/account_banking_sepa_direct_debit/data/pain.008.001.02.xsd b/account_banking_sepa_direct_debit/data/pain.008.001.02.xsd
new file mode 100644
index 000000000..ceafe505f
--- /dev/null
+++ b/account_banking_sepa_direct_debit/data/pain.008.001.02.xsd
@@ -0,0 +1,879 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_banking_sepa_direct_debit/data/pain.008.001.03.xsd b/account_banking_sepa_direct_debit/data/pain.008.001.03.xsd
new file mode 100644
index 000000000..358480dec
--- /dev/null
+++ b/account_banking_sepa_direct_debit/data/pain.008.001.03.xsd
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/account_banking_sepa_direct_debit/data/pain.008.001.04.xsd b/account_banking_sepa_direct_debit/data/pain.008.001.04.xsd
new file mode 100644
index 000000000..ba8c68346
--- /dev/null
+++ b/account_banking_sepa_direct_debit/data/pain.008.001.04.xsd
@@ -0,0 +1,892 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_banking_sepa_direct_debit/data/payment_type_sdd.xml b/account_banking_sepa_direct_debit/data/payment_type_sdd.xml
new file mode 100644
index 000000000..5ce4b5b1b
--- /dev/null
+++ b/account_banking_sepa_direct_debit/data/payment_type_sdd.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+ SEPA Direct Debit v02
+ pain.008.001.02
+
+
+ payment
+
+
+
+ SEPA Direct Debit v03
+ pain.008.001.03
+
+
+ payment
+
+
+
+ SEPA Direct Debit v04
+ pain.008.001.04
+
+
+ payment
+
+
+
+
+
+
diff --git a/account_banking_sepa_direct_debit/security/ir.model.access.csv b/account_banking_sepa_direct_debit/security/ir.model.access.csv
new file mode 100644
index 000000000..0cd579511
--- /dev/null
+++ b/account_banking_sepa_direct_debit/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_banking_export_sdd","Full access on banking.export.sdd","model_banking_export_sdd","account_payment.group_account_payment",1,1,1,1
diff --git a/account_banking_sepa_direct_debit/wizard/__init__.py b/account_banking_sepa_direct_debit/wizard/__init__.py
new file mode 100644
index 000000000..169d0b13d
--- /dev/null
+++ b/account_banking_sepa_direct_debit/wizard/__init__.py
@@ -0,0 +1,23 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# SEPA Direct Debit module for OpenERP
+# Copyright (C) 2013 Akretion (http://www.akretion.com)
+# @author: Alexis de Lattre
+#
+# 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 .
+#
+##############################################################################
+
+import export_sdd
diff --git a/account_banking_sepa_direct_debit/wizard/export_sdd.py b/account_banking_sepa_direct_debit/wizard/export_sdd.py
new file mode 100644
index 000000000..60456ab1a
--- /dev/null
+++ b/account_banking_sepa_direct_debit/wizard/export_sdd.py
@@ -0,0 +1,391 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# SEPA Direct Debit module for OpenERP
+# Copyright (C) 2013 Akretion (http://www.akretion.com)
+# @author: Alexis de Lattre
+#
+# 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
+import base64
+from datetime import datetime, timedelta
+from openerp.tools.translate import _
+from openerp import tools, netsvc
+from lxml import etree
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class banking_export_sdd_wizard(orm.TransientModel):
+ _name = 'banking.export.sdd.wizard'
+ _description = 'Export SEPA Direct Debit XML file'
+ _columns = {
+ 'state': fields.selection([('create', 'Create'), ('finish', 'Finish')],
+ 'State', readonly=True),
+ 'msg_identification': fields.char('Message identification', size=35,
+ # Can't set required=True on the field because it blocks
+ # the launch of the wizard -> I set it as required in the view
+ help='This is the message identification of the entire SEPA XML file. 35 characters max.'),
+ 'batch_booking': fields.boolean('Batch booking',
+ help="If true, the bank statement will display only one debit line for all the wire transfers of the SEPA XML file ; if false, the bank statement will display one debit line per wire transfer of the SEPA XML file."),
+ 'requested_collec_date': fields.date('Requested collection date',
+ help='This is the date on which the collection should be made by the bank. Please keep in mind that banks only execute on working days.'),
+ 'charge_bearer': fields.selection([
+ ('SHAR', 'Shared'),
+ ('CRED', 'Borne by creditor'),
+ ('DEBT', 'Borne by debtor'),
+ ('SLEV', 'Following service level'),
+ ], 'Charge bearer', required=True,
+ help='Shared : transaction charges on the sender side are to be borne by the debtor, transaction charges on the receiver side are to be borne by the creditor (most transfers use this). Borne by creditor : all transaction charges are to be borne by the creditor. Borne by debtor : all transaction charges are to be borne by the debtor. Following service level : transaction charges are to be applied following the rules agreed in the service level and/or scheme.'),
+ 'nb_transactions': fields.related('file_id', 'nb_transactions',
+ type='integer', string='Number of transactions', readonly=True),
+ 'total_amount': fields.related('file_id', 'total_amount', type='float',
+ string='Total amount', readonly=True),
+ 'file_id': fields.many2one('banking.export.sdd', 'SDD XML file', readonly=True),
+ 'file': fields.related('file_id', 'file', string="File", type='binary',
+ readonly=True),
+ 'filename': fields.related('file_id', 'filename', string="Filename",
+ type='char', size=256, readonly=True),
+ 'payment_order_ids': fields.many2many('payment.order',
+ 'wiz_sdd_payorders_rel', 'wizard_id', 'payment_order_id',
+ 'Payment orders', readonly=True),
+ }
+
+ _defaults = {
+ 'charge_bearer': 'SLEV',
+ 'state': 'create',
+ }
+
+
+ def _check_msg_identification(self, cr, uid, ids):
+ '''Check that the msg_identification is unique'''
+ for export_sdd in self.browse(cr, uid, ids):
+ res = self.pool.get('banking.export.sdd').search(cr, uid,
+ [('msg_identification', '=', export_sdd.msg_identification)])
+ if len(res) > 1:
+ return False
+ return True
+
+
+ _constraints = [
+ (_check_msg_identification, "The field 'Message Identification' should be uniue. Another SEPA Direct Debit file already exists with the same 'Message Identification'.", ['msg_identification'])
+ ]
+
+
+ def _validate_iban(self, cr, uid, iban, context=None):
+ '''if IBAN is valid, returns IBAN
+ if IBAN is NOT valid, raises an error message'''
+ partner_bank_obj = self.pool.get('res.partner.bank')
+ if partner_bank_obj.is_iban_valid(cr, uid, iban, context=context):
+ return iban.replace(' ', '')
+ else:
+ raise orm.except_orm(_('Error :'), _("This IBAN is not valid : %s") % iban)
+
+ def create(self, cr, uid, vals, context=None):
+ payment_order_ids = context.get('active_ids', [])
+ vals.update({
+ 'payment_order_ids': [[6, 0, payment_order_ids]],
+ })
+ return super(banking_export_sdd_wizard, self).create(cr, uid,
+ vals, context=context)
+
+
+ def _prepare_field(self, cr, uid, field_name, field_value, max_size=0, sepa_export=False, line=False, context=None):
+ try:
+ value = eval(field_value)
+ except:
+ if line:
+ raise orm.except_orm(_('Error :'), _("Cannot compute the '%s' of the Payment Line with Invoice Reference '%s'.") % (field_name, self.pool['account.invoice'].name_get(cr, uid, [line.ml_inv_ref.id], context=context)[0][1]))
+ else:
+ raise orm.except_orm(_('Error :'), _("Cannot compute the '%s'.") % field_name)
+ if not isinstance(value, (str, unicode)):
+ raise orm.except_orm(_('Field type error :'), _("The '%s' is a(n) %s. It should be a string or unicode.") % (field_name, type(value)))
+ if not value:
+ raise orm.except_orm(_('Error :'), _("The '%s' is empty or 0. It should have a non-null value.") % field_name)
+ if max_size and len(value) > max_size:
+ value = value[0:max_size]
+ return value
+
+
+ def create_sepa(self, cr, uid, ids, context=None):
+ '''
+ Creates the SEPA Direct Debit file. That's the important code !
+ '''
+ payment_order_obj = self.pool.get('payment.order')
+
+ sepa_export = self.browse(cr, uid, ids[0], context=context)
+
+ pain_flavor = sepa_export.payment_order_ids[0].mode.type.code
+ if pain_flavor == 'pain.008.001.02':
+ bic_xml_tag = 'BIC'
+ name_maxsize = 70
+ root_xml_tag = 'CstmrDrctDbtInitn'
+ elif pain_flavor == 'pain.008.001.03':
+ bic_xml_tag = 'BICFI'
+ name_maxsize = 140
+ root_xml_tag = 'CstmrDrctDbtInitn'
+ elif pain_flavor == 'pain.008.001.04':
+ bic_xml_tag = 'BICFI'
+ name_maxsize = 140
+ root_xml_tag = 'CstmrDrctDbtInitn'
+ else:
+ raise orm.except_orm(_('Error :'), _("Payment Type Code '%s' is not supported. The only Payment Type Code supported for SEPA Direct Debit are 'pain.008.001.02', 'pain.008.001.03' and 'pain.008.001.04'.") % pain_flavor)
+ if sepa_export.requested_collec_date:
+ my_requested_collec_date = sepa_export.requested_collec_date
+ else:
+ my_requested_collec_date = datetime.strftime(datetime.today() + timedelta(days=1), '%Y-%m-%d')
+
+ pain_ns = {
+ 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+ None: 'urn:iso:std:iso:20022:tech:xsd:%s' % pain_flavor,
+ }
+
+ root = etree.Element('Document', nsmap=pain_ns)
+ pain_root = etree.SubElement(root, root_xml_tag)
+
+ my_company_name = self._prepare_field(cr, uid, 'Company Name',
+ 'sepa_export.payment_order_ids[0].company_id.name',
+ name_maxsize, sepa_export, context=context)
+
+ # A. Group header
+ group_header_1_0 = etree.SubElement(pain_root, 'GrpHdr')
+ message_identification_1_1 = etree.SubElement(group_header_1_0, 'MsgId')
+ message_identification_1_1.text = sepa_export.msg_identification
+ creation_date_time_1_2 = etree.SubElement(group_header_1_0, 'CreDtTm')
+ creation_date_time_1_2.text = datetime.strftime(datetime.today(), '%Y-%m-%dT%H:%M:%S')
+ nb_of_transactions_1_6 = etree.SubElement(group_header_1_0, 'NbOfTxs')
+ control_sum_1_7 = etree.SubElement(group_header_1_0, 'CtrlSum')
+ initiating_party_1_8 = etree.SubElement(group_header_1_0, 'InitgPty')
+ initiating_party_name = etree.SubElement(initiating_party_1_8, 'Nm')
+ initiating_party_name.text = my_company_name
+
+ # B. Payment info
+ payment_info_2_0 = etree.SubElement(pain_root, 'PmtInf')
+ payment_info_identification_2_1 = etree.SubElement(payment_info_2_0, 'PmtInfId')
+ payment_info_identification_2_1.text = sepa_export.msg_identification
+ payment_method_2_2 = etree.SubElement(payment_info_2_0, 'PmtMtd')
+ payment_method_2_2.text = 'DD'
+ if pain_flavor in ['pain.008.001.02', 'pain.008.001.03', 'pain.008.001.04']:
+ # batch_booking is in "Payment Info" with pain.008.001.02/03
+ batch_booking_2_3 = etree.SubElement(payment_info_2_0, 'BtchBookg')
+ batch_booking_2_3.text = str(sepa_export.batch_booking).lower()
+ # It may seem surprising, but the
+ # "SEPA Core Direct Debit Scheme Customer-to-bank Implementation guidelines"
+ # v6.0 says that control sum and nb_of_transactions should be present
+ # at both "group header" level and "payment info" level
+ if pain_flavor in ['pain.008.001.02', 'pain.008.001.03', 'pain.008.001.04']:
+ nb_of_transactions_2_4 = etree.SubElement(payment_info_2_0, 'NbOfTxs')
+ control_sum_2_5 = etree.SubElement(payment_info_2_0, 'CtrlSum')
+ payment_type_info_2_6 = etree.SubElement(payment_info_2_0, 'PmtTpInf')
+ service_level_2_8 = etree.SubElement(payment_type_info_2_6, 'SvcLvl')
+ service_level_code_2_9 = etree.SubElement(service_level_2_8, 'Cd')
+ service_level_code_2_9.text = 'SEPA'
+ local_instrument_2_11 = etree.SubElement(payment_type_info_2_6, 'LclInstrm')
+ local_instr_code_2_12 = etree.SubElement(local_instrument_2_11, 'Cd')
+ local_instr_code_2_12.text = 'CORE'
+ # TODO : 2.14 Sequence Type MANDATORY => I set it in section C (2.40)
+ # not B (2.14) so that we can have several different Sequence Types
+ # in the same XML file
+ # the Sample XML files show Seq type at C level
+ # BUT it may not be possible,
+ # 1. extract from CIC documentation :
+ # "Attention, les remises présentées devront être scindées par le créancier
+ # par type de séquence"
+ # In the guidelines, they only talk about B level
+ # If ‘Amendment Indicator’ is ‘true’,
+ # and ‘Original Debtor Agent’ is set to ‘SMNDA’,
+ # this message element must indicate ‘FRST
+ # 'FRST' = First ; 'OOFF' = One Off ; 'RCUR' : Recurring
+ # 'FNAL' = Final
+ requested_collec_date_2_18 = etree.SubElement(payment_info_2_0, 'ReqdColltnDt')
+ requested_collec_date_2_18.text = my_requested_collec_date
+ creditor_2_19 = etree.SubElement(payment_info_2_0, 'Cdtr')
+ creditor_name = etree.SubElement(creditor_2_19, 'Nm')
+ creditor_name.text = my_company_name
+ creditor_account_2_20 = etree.SubElement(payment_info_2_0, 'CdtrAcct')
+ creditor_account_id = etree.SubElement(creditor_account_2_20, 'Id')
+ creditor_account_iban = etree.SubElement(creditor_account_id, 'IBAN')
+ creditor_account_iban.text = self._validate_iban(cr, uid,
+ self._prepare_field(cr, uid, 'Company IBAN',
+ 'sepa_export.payment_order_ids[0].mode.bank_id.acc_number',
+ sepa_export=sepa_export, context=context),
+ context=context)
+
+ creditor_agent_2_21 = etree.SubElement(payment_info_2_0, 'CdtrAgt')
+ creditor_agent_institution = etree.SubElement(creditor_agent_2_21, 'FinInstnId')
+ creditor_agent_bic = etree.SubElement(creditor_agent_institution, bic_xml_tag)
+ creditor_agent_bic.text = self._prepare_field(cr, uid, 'Company BIC',
+ 'sepa_export.payment_order_ids[0].mode.bank_id.bank.bic',
+ sepa_export=sepa_export, context=context)
+
+ charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr')
+ charge_bearer_2_24.text = sepa_export.charge_bearer
+
+ creditor_scheme_identification_2_27 = etree.SubElement(payment_info_2_0, 'CdtrSchmeId')
+ csi_id = etree.SubElement(creditor_scheme_identification_2_27, 'Id')
+ csi_orgid = csi_id = etree.SubElement(csi_id, 'OrgId')
+ csi_other = etree.SubElement(csi_orgid, 'Othr')
+ csi_other_id = etree.SubElement(csi_other, 'Id')
+ csi_other_id.text = self._prepare_field(cr, uid,
+ 'SEPA Creditor Identifier',
+ 'sepa_export.payment_order_ids[0].company_id.sepa_creditor_identifier',
+ sepa_export=sepa_export, context=context)
+ csi_scheme_name = etree.SubElement(csi_other, 'SchmeNm')
+ csi_scheme_name_proprietary = etree.SubElement(csi_scheme_name, 'Prtry')
+ csi_scheme_name_proprietary.text = 'SEPA'
+
+ transactions_count = 0
+ total_amount = 0.0
+ amount_control_sum = 0.0
+ # Iterate on payment orders
+ for payment_order in sepa_export.payment_order_ids:
+ total_amount = total_amount + payment_order.total
+ # Iterate each payment lines
+ for line in payment_order.line_ids:
+ transactions_count += 1
+ # C. Direct Debit Transaction Info
+ dd_transaction_info_2_28 = etree.SubElement(payment_info_2_0, 'DrctDbtTxInf')
+ payment_identification_2_29 = etree.SubElement(dd_transaction_info_2_28, 'PmtId')
+ # Instruction identification (2.30) is not mandatory, so we don't use it
+ end2end_identification_2_31 = etree.SubElement(payment_identification_2_29, 'EndToEndId')
+ end2end_identification_2_31.text = self._prepare_field(cr, uid,
+ 'End to End Identification', 'line.communication', 35,
+ line=line, context=context)
+ payment_type_2_32 = etree.SubElement(dd_transaction_info_2_28, 'PmtTpInf')
+ # Sequence Type : do we have to set it at Payment Info level ?
+ #sequence_type_2_40 = etree.SubElement(payment_type_2_32, 'SeqTp')
+ #sequence_type_2_40.text = 'FRST' # TODO
+ currency_name = self._prepare_field(cr, uid, 'Currency Code',
+ 'line.currency.name', 3, line=line, context=context)
+ instructed_amount_2_44 = etree.SubElement(dd_transaction_info_2_28, 'InstdAmt', Ccy=currency_name)
+ instructed_amount_2_44.text = '%.2f' % line.amount_currency
+ amount_control_sum += line.amount_currency
+ dd_transaction_2_46 = etree.SubElement(dd_transaction_info_2_28, 'DrctDbtTx')
+ mandate_related_info_2_47 = etree.SubElement(dd_transaction_2_46, 'MndtRltdInf')
+ mandate_identification_2_48 = etree.SubElement(mandate_related_info_2_47, 'MndtId')
+ mandate_identification_2_48.text = 'RUM1242' # TODO
+ mandate_signature_date_2_49 = etree.SubElement(mandate_related_info_2_47, 'DtOfSgntr')
+ mandate_signature_date_2_49.text = '2013-02-20' # TODO
+ # TODO look at 2.50 "Amendment Indicator
+ debtor_agent_2_70 = etree.SubElement(dd_transaction_info_2_28, 'DbtrAgt')
+ debtor_agent_institution = etree.SubElement(debtor_agent_2_70, 'FinInstnId')
+ debtor_agent_bic = etree.SubElement(debtor_agent_institution, bic_xml_tag)
+ debtor_agent_bic.text = self._prepare_field(cr, uid,
+ 'Customer BIC', 'line.bank_id.bank.bic',
+ line=line, context=context)
+ debtor_2_72 = etree.SubElement(dd_transaction_info_2_28, 'Dbtr')
+ debtor_name = etree.SubElement(debtor_2_72, 'Nm')
+ debtor_name.text = self._prepare_field(cr, uid,
+ 'Customer Name', 'line.partner_id.name',
+ name_maxsize, line=line, context=context)
+ debtor_account_2_73 = etree.SubElement(dd_transaction_info_2_28, 'DbtrAcct')
+ debtor_account_id = etree.SubElement(debtor_account_2_73, 'Id')
+ debtor_account_iban = etree.SubElement(debtor_account_id, 'IBAN')
+ debtor_account_iban.text = self._validate_iban(cr, uid,
+ self._prepare_field(cr, uid, 'Customer IBAN',
+ 'line.bank_id.acc_number', line=line,
+ context=context),
+ context=context)
+ remittance_info_2_88 = etree.SubElement(dd_transaction_info_2_28, 'RmtInf')
+ # switch to Structured (Strdr) ? If we do it, beware that the format is not the same between pain 02 and pain 03
+ remittance_info_unstructured_2_89 = etree.SubElement(remittance_info_2_88, 'Ustrd')
+ remittance_info_unstructured_2_89.text = self._prepare_field(cr, uid,
+ 'Remittance Information', 'line.communication',
+ 140, line=line, context=context)
+
+ if pain_flavor in ['pain.008.001.02', 'pain.008.001.03', 'pain.008.001.04']:
+ nb_of_transactions_1_6.text = nb_of_transactions_2_4.text = str(transactions_count)
+ control_sum_1_7.text = control_sum_2_5.text = '%.2f' % amount_control_sum
+
+
+ xml_string = etree.tostring(root, pretty_print=True, encoding='UTF-8', xml_declaration=True)
+ _logger.debug("Generated SDD XML file in format %s below" % pain_flavor)
+ _logger.debug(xml_string)
+ xsd_etree_obj = etree.parse(tools.file_open('account_banking_sepa_direct_debit/data/%s.xsd' % pain_flavor))
+ official_pain_schema = etree.XMLSchema(xsd_etree_obj)
+ _logger.debug("Printing %s XML Schema definition:" % pain_flavor)
+ _logger.debug(etree.tostring(xsd_etree_obj, pretty_print=True, encoding='UTF-8', xml_declaration=True))
+
+ try:
+ # If I do official_pain_schema.assertValid(root), then I get this
+ # error msg in the exception :
+ # The generated XML file is not valid against the official XML Schema Definition. The generated XML file and the full error have been written in the server logs. Here is the error, which may give you an idea on the cause of the problem : Element 'Document': No matching global declaration available for the validation root.
+ # So I re-import the SEPA XML from the string, and give this
+ # so validation
+ # If you know how I can avoid that, please tell me -- Alexis
+ root_to_validate = etree.fromstring(xml_string)
+ official_pain_schema.assertValid(root_to_validate)
+ except Exception, e:
+ _logger.warning("The XML file is invalid against the XML Schema Definition")
+ _logger.warning(xml_string)
+ _logger.warning(e)
+ raise orm.except_orm(_('Error :'), _('The generated XML file is not valid against the official XML Schema Definition. The generated XML file and the full error have been written in the server logs. Here is the error, which may give you an idea on the cause of the problem : %s') % str(e))
+
+ # CREATE the banking.export.sepa record
+ file_id = self.pool.get('banking.export.sdd').create(cr, uid,
+ {
+ 'msg_identification': sepa_export.msg_identification,
+ 'batch_booking': sepa_export.batch_booking,
+ 'charge_bearer': sepa_export.charge_bearer,
+ 'requested_collec_date': sepa_export.requested_collec_date,
+ 'total_amount': total_amount,
+ 'nb_transactions': transactions_count,
+ 'file': base64.encodestring(xml_string),
+ 'payment_order_ids': [
+ (6, 0, [x.id for x in sepa_export.payment_order_ids])
+ ],
+ }, context=context)
+
+ self.write(cr, uid, ids, {
+ 'file_id': file_id,
+ 'state': 'finish',
+ }, context=context)
+
+ action = {
+ 'name': 'SEPA Direct Debit XML',
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form,tree',
+ 'res_model': self._name,
+ 'res_id': ids[0],
+ 'target': 'new',
+ }
+ return action
+
+
+ def cancel_sepa(self, cr, uid, ids, context=None):
+ '''
+ Cancel the SEPA Direct Debit file: just drop the file
+ '''
+ sepa_export = self.browse(cr, uid, ids[0], context=context)
+ self.pool.get('banking.export.sdd').unlink(cr, uid, sepa_export.file_id.id, context=context)
+ return {'type': 'ir.actions.act_window_close'}
+
+
+ def save_sepa(self, cr, uid, ids, context=None):
+ '''
+ Save the SEPA Direct Debit file: mark all payments in the file as 'sent'.
+ '''
+ sepa_export = self.browse(cr, uid, ids[0], context=context)
+ sepa_file = self.pool.get('banking.export.sdd').write(cr, uid,
+ sepa_export.file_id.id, {'state': 'sent'}, context=context)
+ wf_service = netsvc.LocalService('workflow')
+ for order in sepa_export.payment_order_ids:
+ wf_service.trg_validate(uid, 'payment.order', order.id, 'sent', cr)
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/account_banking_sepa_direct_debit/wizard/export_sdd_view.xml b/account_banking_sepa_direct_debit/wizard/export_sdd_view.xml
new file mode 100644
index 000000000..8357e3f5d
--- /dev/null
+++ b/account_banking_sepa_direct_debit/wizard/export_sdd_view.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ banking.export.sdd.wizard.view
+ banking.export.sdd.wizard
+
+
+
+
+
+
+
From 05319329c63e79e326c84b62441d3ba84f73aaab Mon Sep 17 00:00:00 2001
From: Alexis de Lattre
Date: Tue, 15 Oct 2013 23:29:28 +0200
Subject: [PATCH 02/43] First implementation of mandates, but I still have a
lot of hesitation about the data model so it may change. Manage different
sequence types in the same file ; we just have to separate them in different
payment info blocks.
---
account_banking_sepa_direct_debit/__init__.py | 6 +-
.../__openerp__.py | 2 +
.../account_banking_sdd.py | 85 ++++++
.../account_banking_sdd_view.xml | 102 +++++++
.../account_payment_view.xml | 26 ++
account_banking_sepa_direct_debit/company.py | 6 +-
.../data/mandate_reference_sequence.xml | 21 ++
.../security/ir.model.access.csv | 2 +
.../wizard/__init__.py | 2 +-
.../wizard/export_sdd.py | 256 +++++++++++-------
10 files changed, 407 insertions(+), 101 deletions(-)
create mode 100644 account_banking_sepa_direct_debit/account_payment_view.xml
create mode 100644 account_banking_sepa_direct_debit/data/mandate_reference_sequence.xml
diff --git a/account_banking_sepa_direct_debit/__init__.py b/account_banking_sepa_direct_debit/__init__.py
index bda7501b7..ce46fda33 100644
--- a/account_banking_sepa_direct_debit/__init__.py
+++ b/account_banking_sepa_direct_debit/__init__.py
@@ -20,7 +20,7 @@
#
##############################################################################
-import company
-import wizard
-import account_banking_sdd
+from . import company
+from . import wizard
+from . import account_banking_sdd
diff --git a/account_banking_sepa_direct_debit/__openerp__.py b/account_banking_sepa_direct_debit/__openerp__.py
index fc329a073..2a44e1c06 100644
--- a/account_banking_sepa_direct_debit/__openerp__.py
+++ b/account_banking_sepa_direct_debit/__openerp__.py
@@ -29,9 +29,11 @@
'depends': ['account_direct_debit'],
'data': [
'account_banking_sdd_view.xml',
+ 'account_payment_view.xml',
'company_view.xml',
'wizard/export_sdd_view.xml',
'data/payment_type_sdd.xml',
+ 'data/mandate_reference_sequence.xml',
'security/ir.model.access.csv',
],
'description': '''
diff --git a/account_banking_sepa_direct_debit/account_banking_sdd.py b/account_banking_sepa_direct_debit/account_banking_sdd.py
index aa08918a2..7e5a5d0f3 100644
--- a/account_banking_sepa_direct_debit/account_banking_sdd.py
+++ b/account_banking_sepa_direct_debit/account_banking_sdd.py
@@ -75,3 +75,88 @@ class banking_export_sdd(orm.Model):
'generation_date': fields.date.context_today,
'state': 'draft',
}
+
+
+class sdd_mandate(orm.Model):
+ '''SEPA Direct Debit Mandate'''
+ _name = 'sdd.mandate'
+ _description = __doc__
+ _rec_name = 'unique_mandate_reference'
+ _order = 'signature_date desc'
+
+ _columns = {
+ 'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account'),
+ 'partner_id': fields.related(
+ 'partner_bank_id', 'partner_id', type='many2one',
+ relation='res.partner', string='Partner', readonly=True),
+ 'company_id': fields.many2one('res.company', 'Company', required=True),
+ 'unique_mandate_reference': fields.char(
+ 'Unique Mandate Reference', size=35, readonly=True),
+ 'type': fields.selection([
+ ('recurrent', 'Recurrent'),
+ ('oneoff', 'One-Off'),
+ ], 'Type of Mandate', required=True),
+ 'signature_date': fields.date('Date of Signature of the Mandate'),
+ 'scan': fields.binary('Scan of the mandate'),
+ 'last_debit_date': fields.date('Date of the Last Debit',
+ help="For recurrent mandates, this field is used to know if the SDD will be of type 'First' or 'Recurring'. For one-off mandates, this field is used to know if the SDD has already been used or not."),
+ 'state': fields.selection([
+ ('valid', 'Valid'),
+ ('expired', 'Expired'),
+ ], 'Mandate Status',
+ help="For a recurrent mandate, this field indicate if the mandate is still valid or if it has expired (a recurrent mandate expires if it's not used during 36 months). For a one-off mandate, it expires after its first use."),
+ }
+
+ _sql_constraints = [(
+ 'mandate_ref_company_uniq',
+ 'unique(unique_mandate_reference, company_id)',
+ 'A Mandate with the same reference already exists for this company !'
+ )]
+
+ _defaults = {
+ 'company_id': lambda self, cr, uid, context: \
+ self.pool['res.users'].browse(cr, uid, uid, context=context).\
+ company_id.id,
+ 'unique_mandate_reference': lambda self, cr, uid, context: \
+ self.pool['ir.sequence'].get(cr, uid, 'sdd.mandate.reference'),
+ 'state': 'valid',
+ }
+
+
+class res_partner_bank(orm.Model):
+ _inherit = 'res.partner.bank'
+
+ _columns = {
+ 'sdd_mandate_ids': fields.one2many(
+ 'sdd.mandate', 'partner_bank_id', 'SEPA Direct Debit Mandates'),
+ }
+
+
+class payment_line(orm.Model):
+ _inherit = 'payment.line'
+
+ _columns = {
+ 'sdd_mandate_id': fields.many2one(
+ 'sdd.mandate', 'SEPA Direct Debit Mandate'),
+ }
+
+ def _check_sdd_mandate(self, cr, uid, ids):
+ for payline in self.browse(cr, uid, ids):
+ if payline.sdd_mandate_id and not payline.bank_id:
+ raise orm.except_orm(
+ _('Error :'),
+ _("Missing bank account on the payment line with SEPA\
+ Direct Debit Mandate '%s'."
+ % payline.sdd_mandate_id.unique_mandate_reference))
+ elif payline.sdd_mandate_id and payline.bank_id and payline.sdd_mandate_id.partner_bank_id != payline.bank_id.id:
+ raise orm.except_orm(
+ _('Error :'),
+ _("The SEPA Direct Debit Mandate '%s' is not related??"))
+
+ return True
+
+# _constraints = [
+# (_check_sdd_mandate, "Mandate must be attached to bank account", ['bank_id', 'sdd_mandate_id']),
+# ]
+
+ # TODO inherit create to select the first mandate ??
diff --git a/account_banking_sepa_direct_debit/account_banking_sdd_view.xml b/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
index 6dcfb903f..32a289081 100644
--- a/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
+++ b/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
@@ -79,5 +79,107 @@
view_mode="tree,form"
/>
+
+ sdd.mandate.form
+ sdd.mandate
+
+
+
+
+
+
+ sdd.mandate.tree
+ sdd.mandate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SEPA Direct Debit Mandate
+ sdd.mandate
+ form
+ tree,form
+ {'sdd_mandate_main_view': True}
+
+
+ Click to create a new SEPA Direct Debit Mandate.
+
+ The SEPA Direct Debit Mandate is a document signed by your customer that gives you the autorization to do one or several direct debits on his bank account.
+
+
+
+
+
+
+
+ sdd.mandate.res.partner.bank.form
+ res.partner.bank
+
+
+
+
+
+
+
+
+
+
+
+ sdd.mandate.res.partner.bank.tree
+ res.partner.bank
+
+
+
+
+
+
+
+
+
+
+ sdd.mandate.partner.form
+ res.partner
+
+
+
+
+
+
+
+
diff --git a/account_banking_sepa_direct_debit/account_payment_view.xml b/account_banking_sepa_direct_debit/account_payment_view.xml
new file mode 100644
index 000000000..795f30222
--- /dev/null
+++ b/account_banking_sepa_direct_debit/account_payment_view.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ sdd.payment.order.form
+ payment.order
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_banking_sepa_direct_debit/company.py b/account_banking_sepa_direct_debit/company.py
index d85fc6fd7..aed40f64b 100644
--- a/account_banking_sepa_direct_debit/company.py
+++ b/account_banking_sepa_direct_debit/company.py
@@ -24,15 +24,16 @@ import logging
logger = logging.getLogger(__name__)
+
class res_company(orm.Model):
_inherit = 'res.company'
_columns = {
- 'sepa_creditor_identifier': fields.char('SEPA Creditor Identifier', size=35,
+ 'sepa_creditor_identifier': fields.char(
+ 'SEPA Creditor Identifier', size=35,
help="Enter the Creditor Identifier that has been attributed to your company to make SEPA Direct Debits. This identifier is composed of :\n- your country ISO code (2 letters)\n- a 2-digits checkum\n- a 3-letters business code\n- a country-specific identifier"),
}
-
def is_sepa_creditor_identifier_valid(self, cr, uid, sepa_creditor_identifier, context=None):
"""Check if SEPA Creditor Identifier is valid
@param sepa_creditor_identifier: SEPA Creditor Identifier as str or unicode
@@ -62,7 +63,6 @@ class res_company(orm.Model):
else:
return False
-
def _check_sepa_creditor_identifier(self, cr, uid, ids):
for company in self.browse(cr, uid, ids):
if company.sepa_creditor_identifier:
diff --git a/account_banking_sepa_direct_debit/data/mandate_reference_sequence.xml b/account_banking_sepa_direct_debit/data/mandate_reference_sequence.xml
new file mode 100644
index 000000000..6a3143cca
--- /dev/null
+++ b/account_banking_sepa_direct_debit/data/mandate_reference_sequence.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ SDD Mandate Reference
+ sdd.mandate.reference
+
+
+
+ SDD Mandate Reference
+ sdd.mandate.reference
+ RUM
+
+
+
+
+
+
+
diff --git a/account_banking_sepa_direct_debit/security/ir.model.access.csv b/account_banking_sepa_direct_debit/security/ir.model.access.csv
index 0cd579511..cf78ffb59 100644
--- a/account_banking_sepa_direct_debit/security/ir.model.access.csv
+++ b/account_banking_sepa_direct_debit/security/ir.model.access.csv
@@ -1,2 +1,4 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_banking_export_sdd","Full access on banking.export.sdd","model_banking_export_sdd","account_payment.group_account_payment",1,1,1,1
+"access_sdd_mandate","Full access on sdd.mandate","model_sdd_mandate","account_payment.group_account_payment",1,1,1,1
+"access_sdd_mandate_read","Read access on sdd.mandate","model_sdd_mandate","base.group_user",1,0,0,0
diff --git a/account_banking_sepa_direct_debit/wizard/__init__.py b/account_banking_sepa_direct_debit/wizard/__init__.py
index 169d0b13d..3830e36d9 100644
--- a/account_banking_sepa_direct_debit/wizard/__init__.py
+++ b/account_banking_sepa_direct_debit/wizard/__init__.py
@@ -20,4 +20,4 @@
#
##############################################################################
-import export_sdd
+from . import export_sdd
diff --git a/account_banking_sepa_direct_debit/wizard/export_sdd.py b/account_banking_sepa_direct_debit/wizard/export_sdd.py
index 60456ab1a..3cc06e845 100644
--- a/account_banking_sepa_direct_debit/wizard/export_sdd.py
+++ b/account_banking_sepa_direct_debit/wizard/export_sdd.py
@@ -22,10 +22,10 @@
from openerp.osv import orm, fields
-import base64
-from datetime import datetime, timedelta
from openerp.tools.translate import _
from openerp import tools, netsvc
+import base64
+from datetime import datetime, timedelta
from lxml import etree
import logging
@@ -115,7 +115,7 @@ class banking_export_sdd_wizard(orm.TransientModel):
else:
raise orm.except_orm(_('Error :'), _("Cannot compute the '%s'.") % field_name)
if not isinstance(value, (str, unicode)):
- raise orm.except_orm(_('Field type error :'), _("The '%s' is a(n) %s. It should be a string or unicode.") % (field_name, type(value)))
+ raise orm.except_orm(_('Field type error :'), _("The type of the field '%s' is %s. It should be a string or unicode.") % (field_name, type(value)))
if not value:
raise orm.except_orm(_('Error :'), _("The '%s' is empty or 0. It should have a non-null value.") % field_name)
if max_size and len(value) > max_size:
@@ -175,90 +175,140 @@ class banking_export_sdd_wizard(orm.TransientModel):
initiating_party_name = etree.SubElement(initiating_party_1_8, 'Nm')
initiating_party_name.text = my_company_name
- # B. Payment info
- payment_info_2_0 = etree.SubElement(pain_root, 'PmtInf')
- payment_info_identification_2_1 = etree.SubElement(payment_info_2_0, 'PmtInfId')
- payment_info_identification_2_1.text = sepa_export.msg_identification
- payment_method_2_2 = etree.SubElement(payment_info_2_0, 'PmtMtd')
- payment_method_2_2.text = 'DD'
- if pain_flavor in ['pain.008.001.02', 'pain.008.001.03', 'pain.008.001.04']:
- # batch_booking is in "Payment Info" with pain.008.001.02/03
- batch_booking_2_3 = etree.SubElement(payment_info_2_0, 'BtchBookg')
- batch_booking_2_3.text = str(sepa_export.batch_booking).lower()
- # It may seem surprising, but the
- # "SEPA Core Direct Debit Scheme Customer-to-bank Implementation guidelines"
- # v6.0 says that control sum and nb_of_transactions should be present
- # at both "group header" level and "payment info" level
- if pain_flavor in ['pain.008.001.02', 'pain.008.001.03', 'pain.008.001.04']:
- nb_of_transactions_2_4 = etree.SubElement(payment_info_2_0, 'NbOfTxs')
- control_sum_2_5 = etree.SubElement(payment_info_2_0, 'CtrlSum')
- payment_type_info_2_6 = etree.SubElement(payment_info_2_0, 'PmtTpInf')
- service_level_2_8 = etree.SubElement(payment_type_info_2_6, 'SvcLvl')
- service_level_code_2_9 = etree.SubElement(service_level_2_8, 'Cd')
- service_level_code_2_9.text = 'SEPA'
- local_instrument_2_11 = etree.SubElement(payment_type_info_2_6, 'LclInstrm')
- local_instr_code_2_12 = etree.SubElement(local_instrument_2_11, 'Cd')
- local_instr_code_2_12.text = 'CORE'
- # TODO : 2.14 Sequence Type MANDATORY => I set it in section C (2.40)
- # not B (2.14) so that we can have several different Sequence Types
- # in the same XML file
- # the Sample XML files show Seq type at C level
- # BUT it may not be possible,
- # 1. extract from CIC documentation :
- # "Attention, les remises présentées devront être scindées par le créancier
- # par type de séquence"
- # In the guidelines, they only talk about B level
- # If ‘Amendment Indicator’ is ‘true’,
- # and ‘Original Debtor Agent’ is set to ‘SMNDA’,
- # this message element must indicate ‘FRST
- # 'FRST' = First ; 'OOFF' = One Off ; 'RCUR' : Recurring
- # 'FNAL' = Final
- requested_collec_date_2_18 = etree.SubElement(payment_info_2_0, 'ReqdColltnDt')
- requested_collec_date_2_18.text = my_requested_collec_date
- creditor_2_19 = etree.SubElement(payment_info_2_0, 'Cdtr')
- creditor_name = etree.SubElement(creditor_2_19, 'Nm')
- creditor_name.text = my_company_name
- creditor_account_2_20 = etree.SubElement(payment_info_2_0, 'CdtrAcct')
- creditor_account_id = etree.SubElement(creditor_account_2_20, 'Id')
- creditor_account_iban = etree.SubElement(creditor_account_id, 'IBAN')
- creditor_account_iban.text = self._validate_iban(cr, uid,
- self._prepare_field(cr, uid, 'Company IBAN',
- 'sepa_export.payment_order_ids[0].mode.bank_id.acc_number',
- sepa_export=sepa_export, context=context),
- context=context)
-
- creditor_agent_2_21 = etree.SubElement(payment_info_2_0, 'CdtrAgt')
- creditor_agent_institution = etree.SubElement(creditor_agent_2_21, 'FinInstnId')
- creditor_agent_bic = etree.SubElement(creditor_agent_institution, bic_xml_tag)
- creditor_agent_bic.text = self._prepare_field(cr, uid, 'Company BIC',
- 'sepa_export.payment_order_ids[0].mode.bank_id.bank.bic',
- sepa_export=sepa_export, context=context)
-
- charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr')
- charge_bearer_2_24.text = sepa_export.charge_bearer
-
- creditor_scheme_identification_2_27 = etree.SubElement(payment_info_2_0, 'CdtrSchmeId')
- csi_id = etree.SubElement(creditor_scheme_identification_2_27, 'Id')
- csi_orgid = csi_id = etree.SubElement(csi_id, 'OrgId')
- csi_other = etree.SubElement(csi_orgid, 'Othr')
- csi_other_id = etree.SubElement(csi_other, 'Id')
- csi_other_id.text = self._prepare_field(cr, uid,
- 'SEPA Creditor Identifier',
- 'sepa_export.payment_order_ids[0].company_id.sepa_creditor_identifier',
- sepa_export=sepa_export, context=context)
- csi_scheme_name = etree.SubElement(csi_other, 'SchmeNm')
- csi_scheme_name_proprietary = etree.SubElement(csi_scheme_name, 'Prtry')
- csi_scheme_name_proprietary.text = 'SEPA'
-
- transactions_count = 0
+ transactions_count_1_6 = 0
total_amount = 0.0
- amount_control_sum = 0.0
+ amount_control_sum_1_7 = 0.0
+ first_recur_lines = {}
+ # key = sequence type ; value = list of lines as objects
# Iterate on payment orders
for payment_order in sepa_export.payment_order_ids:
total_amount = total_amount + payment_order.total
# Iterate each payment lines
for line in payment_order.line_ids:
- transactions_count += 1
+ transactions_count_1_6 += 1
+ if not line.sdd_mandate_id:
+ raise orm.except_orm(
+ _('Error:'),
+ _("Missing SEPA Direct Debit mandate on the payment line with partner '%s' and Invoice ref '%s'.")
+ % (line.partner_id.name,
+ line.ml_inv_ref.number))
+ if line.sdd_mandate_id.state != 'valid':
+ raise orm.except_orm(
+ _('Error:'),
+ _("The SEPA Direct Debit mandate with reference '%s' for partner '%s' has expired.")
+ % (line.sdd_mandate_id.unique_mandate_reference,
+ line.sdd_mandate_id.partner_id.name))
+
+ if not line.sdd_mandate_id.signature_date:
+ raise orm.except_orm(
+ _('Error:'),
+ _("Missing signature date on SEPA Direct Debit mandate with reference '%s' for partner '%s'.")
+ % (line.sdd_mandate_id.unique_mandate_reference,
+ line.sdd_mandate_id.partner_id.name))
+ elif line.sdd_mandate_id.signature_date > datetime.today().strftime('%Y-%m-%d'):
+ raise orm.except_orm(
+ _('Error:'),
+ _("The signature date on SEPA Direct Debit mandate with reference '%s' for partner '%s' is '%s', which is in the future !")
+ % (line.sdd_mandate_id.unique_mandate_reference,
+ line.sdd_mandate_id.partner_id.name,
+ line.sdd_mandate_id.signature_date))
+
+ if line.sdd_mandate_id.type == 'oneoff':
+ if not line.sdd_mandate_id.last_debit_date:
+ if first_recur_lines.get('OOFF'):
+ first_recur_lines['OOFF'].append(line)
+ else:
+ first_recur_lines['OOFF'] = [line]
+ else:
+ raise orm.except_orm(
+ _('Error :'),
+ _("The mandate with reference '%s' for partner '%s' has type set to 'One-Off' and it has a last debit date set to '%s', so we can't use it.")
+ % (line.sdd_mandate_id.unique_mandate_reference,
+ line.sdd_mandate_id.partner_id.name,
+ line.sdd_mandate_id.last_debit_date))
+ elif line.sdd_mandate_id.type == 'recurrent':
+ if line.sdd_mandate_id.last_debit_date:
+ if first_recur_lines.get('RCUR'):
+ first_recur_lines['RCUR'].append(line)
+ else:
+ first_recur_lines['RCUR'] = [line]
+ else:
+ if first_recur_lines.get('FRST'):
+ first_recur_lines['FRST'].append(line)
+ else:
+ first_recur_lines['FRST'] = [line]
+
+ for sequence_type, lines in first_recur_lines.items():
+ # B. Payment info
+ payment_info_2_0 = etree.SubElement(pain_root, 'PmtInf')
+ payment_info_identification_2_1 = etree.SubElement(payment_info_2_0, 'PmtInfId')
+ payment_info_identification_2_1.text = sepa_export.msg_identification
+ payment_method_2_2 = etree.SubElement(payment_info_2_0, 'PmtMtd')
+ payment_method_2_2.text = 'DD'
+ # batch_booking is in "Payment Info" with pain.008.001.02/03
+ batch_booking_2_3 = etree.SubElement(payment_info_2_0, 'BtchBookg')
+ batch_booking_2_3.text = str(sepa_export.batch_booking).lower()
+ # The "SEPA Core Direct Debit Scheme Customer-to-bank
+ # Implementation guidelines" v6.0 says that control sum
+ # and nb_of_transactions should be present
+ # at both "group header" level and "payment info" level
+ nb_of_transactions_2_4 = etree.SubElement(payment_info_2_0, 'NbOfTxs')
+ control_sum_2_5 = etree.SubElement(payment_info_2_0, 'CtrlSum')
+ payment_type_info_2_6 = etree.SubElement(payment_info_2_0, 'PmtTpInf')
+ service_level_2_8 = etree.SubElement(payment_type_info_2_6, 'SvcLvl')
+ service_level_code_2_9 = etree.SubElement(service_level_2_8, 'Cd')
+ service_level_code_2_9.text = 'SEPA'
+ local_instrument_2_11 = etree.SubElement(payment_type_info_2_6, 'LclInstrm')
+ local_instr_code_2_12 = etree.SubElement(local_instrument_2_11, 'Cd')
+ local_instr_code_2_12.text = 'CORE'
+ # 2.14 Sequence Type MANDATORY
+ # this message element must indicate ‘FRST
+ # 'FRST' = First ; 'OOFF' = One Off ; 'RCUR' : Recurring
+ # 'FNAL' = Final
+ sequence_type_2_14 = etree.SubElement(payment_type_info_2_6, 'SeqTp')
+ sequence_type_2_14.text = sequence_type
+
+ requested_collec_date_2_18 = etree.SubElement(payment_info_2_0, 'ReqdColltnDt')
+ requested_collec_date_2_18.text = my_requested_collec_date
+ creditor_2_19 = etree.SubElement(payment_info_2_0, 'Cdtr')
+ creditor_name = etree.SubElement(creditor_2_19, 'Nm')
+ creditor_name.text = my_company_name
+ creditor_account_2_20 = etree.SubElement(payment_info_2_0, 'CdtrAcct')
+ creditor_account_id = etree.SubElement(creditor_account_2_20, 'Id')
+ creditor_account_iban = etree.SubElement(creditor_account_id, 'IBAN')
+ creditor_account_iban.text = self._validate_iban(cr, uid,
+ self._prepare_field(cr, uid, 'Company IBAN',
+ 'sepa_export.payment_order_ids[0].mode.bank_id.acc_number',
+ sepa_export=sepa_export, context=context),
+ context=context)
+
+ creditor_agent_2_21 = etree.SubElement(payment_info_2_0, 'CdtrAgt')
+ creditor_agent_institution = etree.SubElement(creditor_agent_2_21, 'FinInstnId')
+ creditor_agent_bic = etree.SubElement(creditor_agent_institution, bic_xml_tag)
+ creditor_agent_bic.text = self._prepare_field(cr, uid, 'Company BIC',
+ 'sepa_export.payment_order_ids[0].mode.bank_id.bank.bic',
+ sepa_export=sepa_export, context=context)
+
+ charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr')
+ charge_bearer_2_24.text = sepa_export.charge_bearer
+
+ creditor_scheme_identification_2_27 = etree.SubElement(payment_info_2_0, 'CdtrSchmeId')
+ csi_id = etree.SubElement(creditor_scheme_identification_2_27, 'Id')
+ csi_orgid = csi_id = etree.SubElement(csi_id, 'OrgId')
+ csi_other = etree.SubElement(csi_orgid, 'Othr')
+ csi_other_id = etree.SubElement(csi_other, 'Id')
+ csi_other_id.text = self._prepare_field(cr, uid,
+ 'SEPA Creditor Identifier',
+ 'sepa_export.payment_order_ids[0].company_id.sepa_creditor_identifier',
+ sepa_export=sepa_export, context=context)
+ csi_scheme_name = etree.SubElement(csi_other, 'SchmeNm')
+ csi_scheme_name_proprietary = etree.SubElement(csi_scheme_name, 'Prtry')
+ csi_scheme_name_proprietary.text = 'SEPA'
+
+ transactions_count_2_4 = 0
+ amount_control_sum_2_5 = 0.0
+ for line in lines:
+ transactions_count_2_4 += 1
# C. Direct Debit Transaction Info
dd_transaction_info_2_28 = etree.SubElement(payment_info_2_0, 'DrctDbtTxInf')
payment_identification_2_29 = etree.SubElement(dd_transaction_info_2_28, 'PmtId')
@@ -268,20 +318,26 @@ class banking_export_sdd_wizard(orm.TransientModel):
'End to End Identification', 'line.communication', 35,
line=line, context=context)
payment_type_2_32 = etree.SubElement(dd_transaction_info_2_28, 'PmtTpInf')
- # Sequence Type : do we have to set it at Payment Info level ?
- #sequence_type_2_40 = etree.SubElement(payment_type_2_32, 'SeqTp')
- #sequence_type_2_40.text = 'FRST' # TODO
currency_name = self._prepare_field(cr, uid, 'Currency Code',
'line.currency.name', 3, line=line, context=context)
instructed_amount_2_44 = etree.SubElement(dd_transaction_info_2_28, 'InstdAmt', Ccy=currency_name)
instructed_amount_2_44.text = '%.2f' % line.amount_currency
- amount_control_sum += line.amount_currency
+ amount_control_sum_1_7 += line.amount_currency
+ amount_control_sum_2_5 += line.amount_currency
dd_transaction_2_46 = etree.SubElement(dd_transaction_info_2_28, 'DrctDbtTx')
mandate_related_info_2_47 = etree.SubElement(dd_transaction_2_46, 'MndtRltdInf')
mandate_identification_2_48 = etree.SubElement(mandate_related_info_2_47, 'MndtId')
- mandate_identification_2_48.text = 'RUM1242' # TODO
- mandate_signature_date_2_49 = etree.SubElement(mandate_related_info_2_47, 'DtOfSgntr')
- mandate_signature_date_2_49.text = '2013-02-20' # TODO
+ mandate_identification_2_48.text = self._prepare_field(
+ cr, uid, 'Unique Mandate Reference',
+ 'line.sdd_mandate_id.unique_mandate_reference',
+ 35, line=line, context=context)
+ mandate_signature_date_2_49 = etree.SubElement(
+ mandate_related_info_2_47, 'DtOfSgntr')
+ mandate_signature_date_2_49.text = self._prepare_field(
+ cr, uid, 'Mandate Signature Date',
+ 'line.sdd_mandate_id.signature_date', 10,
+ line=line, context=context)
+
# TODO look at 2.50 "Amendment Indicator
debtor_agent_2_70 = etree.SubElement(dd_transaction_info_2_28, 'DbtrAgt')
debtor_agent_institution = etree.SubElement(debtor_agent_2_70, 'FinInstnId')
@@ -308,10 +364,10 @@ class banking_export_sdd_wizard(orm.TransientModel):
remittance_info_unstructured_2_89.text = self._prepare_field(cr, uid,
'Remittance Information', 'line.communication',
140, line=line, context=context)
-
- if pain_flavor in ['pain.008.001.02', 'pain.008.001.03', 'pain.008.001.04']:
- nb_of_transactions_1_6.text = nb_of_transactions_2_4.text = str(transactions_count)
- control_sum_1_7.text = control_sum_2_5.text = '%.2f' % amount_control_sum
+ nb_of_transactions_2_4.text = str(transactions_count_2_4)
+ control_sum_2_5.text = '%.2f' % amount_control_sum_2_5
+ nb_of_transactions_1_6.text = str(transactions_count_1_6)
+ control_sum_1_7.text = '%.2f' % amount_control_sum_1_7
xml_string = etree.tostring(root, pretty_print=True, encoding='UTF-8', xml_declaration=True)
@@ -345,7 +401,7 @@ class banking_export_sdd_wizard(orm.TransientModel):
'charge_bearer': sepa_export.charge_bearer,
'requested_collec_date': sepa_export.requested_collec_date,
'total_amount': total_amount,
- 'nb_transactions': transactions_count,
+ 'nb_transactions': transactions_count_1_6,
'file': base64.encodestring(xml_string),
'payment_order_ids': [
(6, 0, [x.id for x in sepa_export.payment_order_ids])
@@ -374,13 +430,15 @@ class banking_export_sdd_wizard(orm.TransientModel):
Cancel the SEPA Direct Debit file: just drop the file
'''
sepa_export = self.browse(cr, uid, ids[0], context=context)
- self.pool.get('banking.export.sdd').unlink(cr, uid, sepa_export.file_id.id, context=context)
+ self.pool.get('banking.export.sdd').unlink(
+ cr, uid, sepa_export.file_id.id, context=context)
return {'type': 'ir.actions.act_window_close'}
def save_sepa(self, cr, uid, ids, context=None):
'''
Save the SEPA Direct Debit file: mark all payments in the file as 'sent'.
+ Write 'last debit date' on mandate and set oneoff mandate to expired
'''
sepa_export = self.browse(cr, uid, ids[0], context=context)
sepa_file = self.pool.get('banking.export.sdd').write(cr, uid,
@@ -388,4 +446,14 @@ class banking_export_sdd_wizard(orm.TransientModel):
wf_service = netsvc.LocalService('workflow')
for order in sepa_export.payment_order_ids:
wf_service.trg_validate(uid, 'payment.order', order.id, 'sent', cr)
+ mandate_ids = [line.sdd_mandate_id.id for line in order.line_ids]
+ self.pool['sdd.mandate'].write(
+ cr, uid, mandate_ids, {
+ 'last_debit_date': datetime.today().strftime('%Y-%m-%d')
+ },
+ context=context)
+ oneoff_mandate_ids = [line.sdd_mandate_id.id for line in order.line_ids if line.sdd_mandate_id.type == 'oneoff']
+ self.pool['sdd.mandate'].write(
+ cr, uid, oneoff_mandate_ids, {'state': 'expired'},
+ context=context)
return {'type': 'ir.actions.act_window_close'}
From 819be28513271442002100b49792e2246378dd0c Mon Sep 17 00:00:00 2001
From: Alexis de Lattre
Date: Sat, 19 Oct 2013 17:29:17 +0200
Subject: [PATCH 03/43] [FIX] correct parent menu entry Remove unused variables
---
account_banking_sepa_direct_debit/account_banking_sdd.py | 1 -
.../account_banking_sdd_view.xml | 2 +-
account_banking_sepa_direct_debit/wizard/export_sdd.py | 6 ++----
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/account_banking_sepa_direct_debit/account_banking_sdd.py b/account_banking_sepa_direct_debit/account_banking_sdd.py
index 7e5a5d0f3..55c5921fd 100644
--- a/account_banking_sepa_direct_debit/account_banking_sdd.py
+++ b/account_banking_sepa_direct_debit/account_banking_sdd.py
@@ -20,7 +20,6 @@
##############################################################################
from openerp.osv import orm, fields
-import time
from openerp.tools.translate import _
from openerp.addons.decimal_precision import decimal_precision as dp
diff --git a/account_banking_sepa_direct_debit/account_banking_sdd_view.xml b/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
index 32a289081..2f0d64c9d 100644
--- a/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
+++ b/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
@@ -65,7 +65,7 @@
diff --git a/account_banking_sepa_direct_debit/wizard/export_sdd.py b/account_banking_sepa_direct_debit/wizard/export_sdd.py
index 3cc06e845..66e478188 100644
--- a/account_banking_sepa_direct_debit/wizard/export_sdd.py
+++ b/account_banking_sepa_direct_debit/wizard/export_sdd.py
@@ -127,8 +127,6 @@ class banking_export_sdd_wizard(orm.TransientModel):
'''
Creates the SEPA Direct Debit file. That's the important code !
'''
- payment_order_obj = self.pool.get('payment.order')
-
sepa_export = self.browse(cr, uid, ids[0], context=context)
pain_flavor = sepa_export.payment_order_ids[0].mode.type.code
@@ -441,11 +439,11 @@ class banking_export_sdd_wizard(orm.TransientModel):
Write 'last debit date' on mandate and set oneoff mandate to expired
'''
sepa_export = self.browse(cr, uid, ids[0], context=context)
- sepa_file = self.pool.get('banking.export.sdd').write(cr, uid,
+ self.pool.get('banking.export.sdd').write(cr, uid,
sepa_export.file_id.id, {'state': 'sent'}, context=context)
wf_service = netsvc.LocalService('workflow')
for order in sepa_export.payment_order_ids:
- wf_service.trg_validate(uid, 'payment.order', order.id, 'sent', cr)
+ wf_service.trg_validate(uid, 'payment.order', order.id, 'done', cr)
mandate_ids = [line.sdd_mandate_id.id for line in order.line_ids]
self.pool['sdd.mandate'].write(
cr, uid, mandate_ids, {
From 059c28ac04afa9aedc80b1675cc4905c6ded9279 Mon Sep 17 00:00:00 2001
From: Alexis de Lattre
Date: Wed, 23 Oct 2013 00:25:06 +0200
Subject: [PATCH 04/43] 2 modifications following a real-life SDD with a French
bank: - convert accented chars to ascii chars (via the unidecode lib) - use
"PrvtId" instead of "OrgId" in the XML Use the sequence of payment.order as
the "Message identification" of the XML file (advantages : it is unique,
users can easily customize the sequence and users can easily find the payment
corresponding to the "Message Identification" in OpenERP). It is also used as
the Payment Identification, combined with the sequence type. Use the sequence
of payment.line in the "EndtoEnd Identification" field. Reduce flake8
warnings.
---
account_banking_sepa_direct_debit/__init__.py | 1 -
.../account_banking_sdd.py | 50 +-
.../account_banking_sdd_view.xml | 3 +-
account_banking_sepa_direct_debit/company.py | 22 +-
.../data/mandate_reference_sequence.xml | 2 +-
.../account_banking_sepa_direct_debit.pot | 494 ++++++++++++++++++
.../wizard/export_sdd.py | 73 ++-
.../wizard/export_sdd_view.xml | 2 -
8 files changed, 567 insertions(+), 80 deletions(-)
create mode 100644 account_banking_sepa_direct_debit/i18n/account_banking_sepa_direct_debit.pot
diff --git a/account_banking_sepa_direct_debit/__init__.py b/account_banking_sepa_direct_debit/__init__.py
index ce46fda33..f852fb7bd 100644
--- a/account_banking_sepa_direct_debit/__init__.py
+++ b/account_banking_sepa_direct_debit/__init__.py
@@ -23,4 +23,3 @@
from . import company
from . import wizard
from . import account_banking_sdd
-
diff --git a/account_banking_sepa_direct_debit/account_banking_sdd.py b/account_banking_sepa_direct_debit/account_banking_sdd.py
index 55c5921fd..0a57ee839 100644
--- a/account_banking_sepa_direct_debit/account_banking_sdd.py
+++ b/account_banking_sepa_direct_debit/account_banking_sdd.py
@@ -22,18 +22,19 @@
from openerp.osv import orm, fields
from openerp.tools.translate import _
from openerp.addons.decimal_precision import decimal_precision as dp
+from unidecode import unidecode
class banking_export_sdd(orm.Model):
'''SEPA Direct Debit export'''
_name = 'banking.export.sdd'
_description = __doc__
- _rec_name = 'msg_identification'
+ _rec_name = 'filename'
def _generate_filename(self, cr, uid, ids, name, arg, context=None):
res = {}
for sepa_file in self.browse(cr, uid, ids, context=context):
- res[sepa_file.id] = 'sdd_' + (sepa_file.msg_identification or '') + '.xml'
+ res[sepa_file.id] = 'sdd_%s.xml' % (sepa_file.payment_order_ids[0].reference and unidecode(sepa_file.payment_order_ids[0].reference.replace('/', '-')) or 'error')
return res
_columns = {
@@ -43,13 +44,15 @@ class banking_export_sdd(orm.Model):
'banking_export_sepa_id', 'account_order_id',
'Payment orders',
readonly=True),
- 'requested_collec_date': fields.date('Requested collection date', readonly=True),
- 'nb_transactions': fields.integer('Number of transactions', readonly=True),
- 'total_amount': fields.float('Total amount',
- digits_compute=dp.get_precision('Account'), readonly=True),
- 'msg_identification': fields.char('Message identification', size=35,
+ 'requested_collec_date': fields.date(
+ 'Requested collection date', readonly=True),
+ 'nb_transactions': fields.integer(
+ 'Number of transactions', readonly=True),
+ 'total_amount': fields.float(
+ 'Total amount', digits_compute=dp.get_precision('Account'),
readonly=True),
- 'batch_booking': fields.boolean('Batch booking', readonly=True,
+ 'batch_booking': fields.boolean(
+ 'Batch booking', readonly=True,
help="If true, the bank statement will display only one credit line for all the direct debits of the SEPA XML file ; if false, the bank statement will display one credit line per direct debit of the SEPA XML file."),
'charge_bearer': fields.selection([
('SHAR', 'Shared'),
@@ -58,15 +61,15 @@ class banking_export_sdd(orm.Model):
('SLEV', 'Following service level'),
], 'Charge bearer', readonly=True,
help='Shared : transaction charges on the sender side are to be borne by the debtor, transaction charges on the receiver side are to be borne by the creditor (most transfers use this). Borne by creditor : all transaction charges are to be borne by the creditor. Borne by debtor : all transaction charges are to be borne by the debtor. Following service level : transaction charges are to be applied following the rules agreed in the service level and/or scheme.'),
- 'generation_date': fields.datetime('Generation date',
- readonly=True),
+ 'generation_date': fields.datetime('Generation date', readonly=True),
'file': fields.binary('SEPA XML file', readonly=True),
- 'filename': fields.function(_generate_filename, type='char', size=256,
- method=True, string='Filename', readonly=True),
+ 'filename': fields.function(
+ _generate_filename, type='char', size=256,
+ string='Filename', readonly=True, store=True),
'state': fields.selection([
- ('draft', 'Draft'),
- ('sent', 'Sent'),
- ('done', 'Reconciled'),
+ ('draft', 'Draft'),
+ ('sent', 'Sent'),
+ ('done', 'Reconciled'),
], 'State', readonly=True),
}
@@ -97,7 +100,8 @@ class sdd_mandate(orm.Model):
], 'Type of Mandate', required=True),
'signature_date': fields.date('Date of Signature of the Mandate'),
'scan': fields.binary('Scan of the mandate'),
- 'last_debit_date': fields.date('Date of the Last Debit',
+ 'last_debit_date': fields.date(
+ 'Date of the Last Debit',
help="For recurrent mandates, this field is used to know if the SDD will be of type 'First' or 'Recurring'. For one-off mandates, this field is used to know if the SDD has already been used or not."),
'state': fields.selection([
('valid', 'Valid'),
@@ -113,10 +117,10 @@ class sdd_mandate(orm.Model):
)]
_defaults = {
- 'company_id': lambda self, cr, uid, context: \
+ 'company_id': lambda self, cr, uid, context:
self.pool['res.users'].browse(cr, uid, uid, context=context).\
company_id.id,
- 'unique_mandate_reference': lambda self, cr, uid, context: \
+ 'unique_mandate_reference': lambda self, cr, uid, context:
self.pool['ir.sequence'].get(cr, uid, 'sdd.mandate.reference'),
'state': 'valid',
}
@@ -143,15 +147,13 @@ class payment_line(orm.Model):
for payline in self.browse(cr, uid, ids):
if payline.sdd_mandate_id and not payline.bank_id:
raise orm.except_orm(
- _('Error :'),
- _("Missing bank account on the payment line with SEPA\
- Direct Debit Mandate '%s'."
- % payline.sdd_mandate_id.unique_mandate_reference))
+ _('Error:'),
+ _("Missing bank account on the payment line with SEPA Direct Debit Mandate '%s'.")
+ % payline.sdd_mandate_id.unique_mandate_reference)
elif payline.sdd_mandate_id and payline.bank_id and payline.sdd_mandate_id.partner_bank_id != payline.bank_id.id:
raise orm.except_orm(
- _('Error :'),
+ _('Error:'),
_("The SEPA Direct Debit Mandate '%s' is not related??"))
-
return True
# _constraints = [
diff --git a/account_banking_sepa_direct_debit/account_banking_sdd_view.xml b/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
index 2f0d64c9d..8249da13e 100644
--- a/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
+++ b/account_banking_sepa_direct_debit/account_banking_sdd_view.xml
@@ -14,7 +14,6 @@
\n"
" "
msgstr ""
+"\n"
+" Klik voor het maken van een nieuwe SEPA incasso machtiging.\n"
+"
\n"
+" Een SEPA incasso machtiging is een document ondertekend door uw "
+"klant, welke u toestemming geeft om incasso's uit te voeren op zijn "
+"bankrekening.\n"
+"
\n"
+" "
#. module: account_banking_sepa_direct_debit
#: view:banking.export.sdd:0
@@ -614,7 +667,7 @@ msgstr "Annuleer"
#: view:sdd.mandate:0
#: field:sdd.mandate,payment_line_ids:0
msgid "Related Payment Lines"
-msgstr ""
+msgstr "Gerelateerde betaalregels"
#. module: account_banking_sepa_direct_debit
#: view:sdd.mandate:0
@@ -625,22 +678,22 @@ msgstr "Terugkerend"
#. module: account_banking_sepa_direct_debit
#: field:sdd.mandate,type:0
msgid "Type of Mandate"
-msgstr ""
+msgstr "Soort machtiging"
#. module: account_banking_sepa_direct_debit
#: model:mail.message.subtype,name:account_banking_sepa_direct_debit.mandate_valid
msgid "Mandate Validated"
-msgstr ""
+msgstr "Machtiging bevestigd"
#. module: account_banking_sepa_direct_debit
#: field:banking.export.sdd,file:0
msgid "SEPA File"
-msgstr ""
+msgstr "SEPA bestand"
#. module: account_banking_sepa_direct_debit
#: field:res.company,sepa_creditor_identifier:0
msgid "SEPA Creditor Identifier"
-msgstr ""
+msgstr "SEPA Incassant-ID"
#. module: account_banking_sepa_direct_debit
#: model:ir.model,name:account_banking_sepa_direct_debit.model_banking_export_sdd
@@ -665,12 +718,15 @@ msgid ""
"You must set the 'Original Mandate Identification' on the recurrent mandate "
"'%s' which is not marked as 'Migrated to SEPA'."
msgstr ""
+"U dient de 'Originele machtiging identificatie' in te stellen op de "
+"herhalende machtiging '%s' welke niet is gemarkeerd als 'Gemigreerd naar "
+"SEPA'"
#. module: account_banking_sepa_direct_debit
#: model:mail.message.subtype,description:account_banking_sepa_direct_debit.recurrent_sequence_type_first
#: model:mail.message.subtype,name:account_banking_sepa_direct_debit.recurrent_sequence_type_first
msgid "Sequence Type set to First"
-msgstr ""
+msgstr "Reeks ingesteld op eerste"
#. module: account_banking_sepa_direct_debit
#: code:addons/account_banking_sepa_direct_debit/account_banking_sdd.py:291
@@ -679,6 +735,8 @@ msgid ""
"As you changed the bank account attached to this mandate, the 'Sequence "
"Type' has been set back to 'First'."
msgstr ""
+"Omdat u de gekoppelde bankrekening heeft gewijzigd is de reeks terug gezet "
+"naar 'Eerste'."
#. module: account_banking_sepa_direct_debit
#: help:sdd.mandate,message_ids:0
@@ -688,7 +746,7 @@ msgstr "Berichten en communicatie historie"
#. module: account_banking_sepa_direct_debit
#: view:sdd.mandate:0
msgid "Search SEPA Direct Debit Mandates"
-msgstr ""
+msgstr "Zoek SEPA incasso machtigingen"
#. module: account_banking_sepa_direct_debit
#: field:banking.export.sdd.wizard,file:0