~therp-nl/banking-addons/6.1-empty_message_direct_debit

« back to all changes in this revision

Viewing changes to account_banking_sepa_credit_transfer/wizard/export_sepa.py

  • Committer: Holger Brunn
  • Author(s): alexis.delattre at akretion
  • Date: 2013-06-24 10:22:12 UTC
  • mfrom: (136.3.16 banking-addons-61-sepa)
  • Revision ID: hbrunn@therp.nl-20130624102212-5q78rt5h2dqbf2ai
[ADD] account_banking_sepa_credit_transfer implementing SEPA SCT pain 001.001.04

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- encoding: utf-8 -*-
 
2
##############################################################################
 
3
#
 
4
#    SEPA Credit Transfer module for OpenERP
 
5
#    Copyright (C) 2010-2013 Akretion (http://www.akretion.com)
 
6
#    @author: Alexis de Lattre <alexis.delattre@akretion.com>
 
7
#
 
8
#    This program is free software: you can redistribute it and/or modify
 
9
#    it under the terms of the GNU Affero General Public License as
 
10
#    published by the Free Software Foundation, either version 3 of the
 
11
#    License, or (at your option) any later version.
 
12
#
 
13
#    This program is distributed in the hope that it will be useful,
 
14
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
#    GNU Affero General Public License for more details.
 
17
#
 
18
#    You should have received a copy of the GNU Affero General Public License
 
19
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
20
#
 
21
##############################################################################
 
22
 
 
23
 
 
24
from osv import osv, fields
 
25
import base64
 
26
from datetime import datetime, timedelta
 
27
from tools.translate import _
 
28
import tools
 
29
from lxml import etree
 
30
import logging
 
31
import netsvc
 
32
 
 
33
_logger = logging.getLogger(__name__)
 
34
 
 
35
 
 
36
class banking_export_sepa_wizard(osv.osv_memory):
 
37
    _name = 'banking.export.sepa.wizard'
 
38
    _description = 'Export SEPA Credit Transfer XML file'
 
39
    _columns = {
 
40
        'state': fields.selection([('create', 'Create'), ('finish', 'Finish')],
 
41
            'State', readonly=True),
 
42
        'msg_identification': fields.char('Message identification', size=35,
 
43
            # Can't set required=True on the field because it blocks
 
44
            # the launch of the wizard -> I set it as required in the view
 
45
            help='This is the message identification of the entire SEPA XML file. 35 characters max.'),
 
46
        'batch_booking': fields.boolean('Batch booking',
 
47
            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."),
 
48
        'prefered_exec_date': fields.date('Prefered execution date',
 
49
            help='This is the date on which the file should be processed by the bank. Please keep in mind that banks only execute on working days and typically use a delay of two days between execution date and effective transfer date.'),
 
50
        'charge_bearer': fields.selection([
 
51
            ('SHAR', 'Shared'),
 
52
            ('CRED', 'Borne by creditor'),
 
53
            ('DEBT', 'Borne by debtor'),
 
54
            ('SLEV', 'Following service level'),
 
55
            ], 'Charge bearer', required=True,
 
56
            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.'),
 
57
        'nb_transactions': fields.related('file_id', 'nb_transactions',
 
58
            type='integer', string='Number of transactions', readonly=True),
 
59
        'total_amount': fields.related('file_id', 'total_amount', type='float',
 
60
            string='Total amount', readonly=True),
 
61
        'file_id': fields.many2one('banking.export.sepa', 'SEPA XML file', readonly=True),
 
62
        'file': fields.related('file_id', 'file', string="File", type='binary',
 
63
            readonly=True),
 
64
        'filename': fields.related('file_id', 'filename', string="Filename",
 
65
            type='char', size=256, readonly=True),
 
66
        'payment_order_ids': fields.many2many('payment.order',
 
67
            'wiz_sepa_payorders_rel', 'wizard_id', 'payment_order_id',
 
68
            'Payment orders', readonly=True),
 
69
        }
 
70
 
 
71
    _defaults = {
 
72
        'charge_bearer': 'SLEV',
 
73
        'state': 'create',
 
74
        }
 
75
 
 
76
 
 
77
    def _limit_size(self, cr, uid, field, max_size, context=None):
 
78
        '''Limit size of strings to respect the PAIN standard'''
 
79
        max_size = int(max_size)
 
80
        return field[0:max_size]
 
81
 
 
82
 
 
83
    def _validate_iban(self, cr, uid, iban, context=None):
 
84
        '''if IBAN is valid, returns IBAN
 
85
        if IBAN is NOT valid, raises an error message'''
 
86
        partner_bank_obj = self.pool.get('res.partner.bank')
 
87
        if partner_bank_obj.is_iban_valid(cr, uid, iban, context=context):
 
88
            return iban.replace(' ', '')
 
89
        else:
 
90
            raise osv.except_osv(_('Error :'), _("This IBAN is not valid : %s") % iban)
 
91
 
 
92
    def create(self, cr, uid, vals, context=None):
 
93
        payment_order_ids = context.get('active_ids', [])
 
94
        vals.update({
 
95
            'payment_order_ids': [[6, 0, payment_order_ids]],
 
96
        })
 
97
        return super(banking_export_sepa_wizard, self).create(cr, uid,
 
98
            vals, context=context)
 
99
 
 
100
 
 
101
    def create_sepa(self, cr, uid, ids, context=None):
 
102
        '''
 
103
        Creates the SEPA Credit Transfer file. That's the important code !
 
104
        '''
 
105
        payment_order_obj = self.pool.get('payment.order')
 
106
 
 
107
        sepa_export = self.browse(cr, uid, ids[0], context=context)
 
108
 
 
109
        my_company_name = sepa_export.payment_order_ids[0].mode.bank_id.partner_id.name
 
110
        my_company_iban = self._validate_iban(cr, uid, sepa_export.payment_order_ids[0].mode.bank_id.iban, context=context)
 
111
        my_company_bic = sepa_export.payment_order_ids[0].mode.bank_id.bank.bic
 
112
        #my_company_country_code = sepa_export.payment_order_ids[0].mode.bank_id.partner_id.address[0].country_id.code
 
113
        #my_company_city = sepa_export.payment_order_ids[0].mode.bank_id.partner_id.address[0].city
 
114
        #my_company_street1 = sepa_export.payment_order_ids[0].mode.bank_id.partner_id.address[0].street
 
115
        pain_flavor = sepa_export.payment_order_ids[0].mode.type.code
 
116
        if pain_flavor == 'pain.001.001.02':
 
117
            bic_xml_tag = 'BIC'
 
118
            name_maxsize = 70
 
119
            root_xml_tag = 'pain.001.001.02'
 
120
        elif pain_flavor == 'pain.001.001.03':
 
121
            bic_xml_tag = 'BIC'
 
122
            # size 70 -> 140 for <Nm> with pain.001.001.03
 
123
            # BUT the European Payment Council, in the document
 
124
            # "SEPA Credit Transfer Scheme Customer-to-bank Implementation guidelines" v6.0
 
125
            # available on http://www.europeanpaymentscouncil.eu/knowledge_bank.cfm
 
126
            # says that 'Nm' should be limited to 70
 
127
            # so we follow the "European Payment Council" and we put 70 and not 140
 
128
            name_maxsize = 70
 
129
            root_xml_tag = 'CstmrCdtTrfInitn'
 
130
        elif pain_flavor == 'pain.001.001.04':
 
131
            bic_xml_tag = 'BICFI'
 
132
            name_maxsize = 140
 
133
            root_xml_tag = 'CstmrCdtTrfInitn'
 
134
        else:
 
135
            raise osv.except_osv(_('Error :'), _("Payment Type Code '%s' is not supported. The only Payment Type Codes supported for SEPA Credit Transfers are 'pain.001.001.02', 'pain.001.001.03' and 'pain.001.001.04'.") % pain_flavor)
 
136
        if sepa_export.batch_booking:
 
137
            my_batch_booking = 'true'
 
138
        else:
 
139
            my_batch_booking = 'false'
 
140
        my_msg_identification = sepa_export.msg_identification
 
141
        if sepa_export.prefered_exec_date:
 
142
            my_requested_exec_date = sepa_export.prefered_exec_date
 
143
        else:
 
144
            my_requested_exec_date = datetime.strftime(datetime.today() + timedelta(days=1), '%Y-%m-%d')
 
145
 
 
146
        pain_ns = {
 
147
            'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
 
148
            None: 'urn:iso:std:iso:20022:tech:xsd:%s' % pain_flavor,
 
149
            }
 
150
 
 
151
        root = etree.Element('Document', nsmap=pain_ns)
 
152
        pain_root = etree.SubElement(root, root_xml_tag)
 
153
        # A. Group header
 
154
        group_header = etree.SubElement(pain_root, 'GrpHdr')
 
155
        message_identification = etree.SubElement(group_header, 'MsgId')
 
156
        message_identification.text = self._limit_size(cr, uid, my_msg_identification, 35, context=context)
 
157
        creation_date_time = etree.SubElement(group_header, 'CreDtTm')
 
158
        creation_date_time.text = datetime.strftime(datetime.today(), '%Y-%m-%dT%H:%M:%S')
 
159
        if pain_flavor == 'pain.001.001.02':
 
160
            # batch_booking is in "Group header" with pain.001.001.02
 
161
            # and in "Payment info" in pain.001.001.03/04
 
162
            batch_booking = etree.SubElement(group_header, 'BtchBookg')
 
163
            batch_booking.text = my_batch_booking
 
164
        nb_of_transactions_grphdr = etree.SubElement(group_header, 'NbOfTxs')
 
165
        control_sum_grphdr = etree.SubElement(group_header, 'CtrlSum')
 
166
        # Grpg removed in pain.001.001.03
 
167
        if pain_flavor == 'pain.001.001.02':
 
168
            grouping = etree.SubElement(group_header, 'Grpg')
 
169
            grouping.text = 'GRPD'
 
170
        initiating_party = etree.SubElement(group_header, 'InitgPty')
 
171
        initiating_party_name = etree.SubElement(initiating_party, 'Nm')
 
172
        initiating_party_name.text = self._limit_size(cr, uid, my_company_name, name_maxsize, context=context)
 
173
        # B. Payment info
 
174
        payment_info = etree.SubElement(pain_root, 'PmtInf')
 
175
        payment_info_identification = etree.SubElement(payment_info, 'PmtInfId')
 
176
        payment_info_identification.text = self._limit_size(cr, uid, my_msg_identification, 35, context=context)
 
177
        payment_method = etree.SubElement(payment_info, 'PmtMtd')
 
178
        payment_method.text = 'TRF'
 
179
        if pain_flavor in ['pain.001.001.03', 'pain.001.001.04']:
 
180
            # batch_booking is in "Group header" with pain.001.001.02
 
181
            # and in "Payment info" in pain.001.001.03/04
 
182
            batch_booking = etree.SubElement(payment_info, 'BtchBookg')
 
183
            batch_booking.text = my_batch_booking
 
184
        # It may seem surprising, but the
 
185
        # "SEPA Credit Transfer Scheme Customer-to-bank Implementation guidelines"
 
186
        # v6.0 says that control sum and nb_of_transactions should be present
 
187
        # at both "group header" level and "payment info" level
 
188
        # This seems to be confirmed by the tests carried out at
 
189
        # BNP Paribas in PAIN v001.001.03
 
190
        if pain_flavor in ['pain.001.001.03', 'pain.001.001.04']:
 
191
            nb_of_transactions_pmtinf = etree.SubElement(payment_info, 'NbOfTxs')
 
192
            control_sum_pmtinf = etree.SubElement(payment_info, 'CtrlSum')
 
193
        payment_type_info = etree.SubElement(payment_info, 'PmtTpInf')
 
194
        service_level = etree.SubElement(payment_type_info, 'SvcLvl')
 
195
        service_level_code = etree.SubElement(service_level, 'Cd')
 
196
        service_level_code.text = 'SEPA'
 
197
        requested_exec_date = etree.SubElement(payment_info, 'ReqdExctnDt')
 
198
        requested_exec_date.text = my_requested_exec_date
 
199
        debtor = etree.SubElement(payment_info, 'Dbtr')
 
200
        debtor_name = etree.SubElement(debtor, 'Nm')
 
201
        debtor_name.text = self._limit_size(cr, uid, my_company_name, name_maxsize, context=context)
 
202
#        debtor_address = etree.SubElement(debtor, 'PstlAdr')
 
203
#        debtor_street = etree.SubElement(debtor_address, 'AdrLine')
 
204
#        debtor_street.text = my_company_street1
 
205
#        debtor_city = etree.SubElement(debtor_address, 'AdrLine')
 
206
#        debtor_city.text = my_company_city
 
207
#        debtor_country = etree.SubElement(debtor_address, 'Ctry')
 
208
#        debtor_country.text = my_company_country_code
 
209
        debtor_account = etree.SubElement(payment_info, 'DbtrAcct')
 
210
        debtor_account_id = etree.SubElement(debtor_account, 'Id')
 
211
        debtor_account_iban = etree.SubElement(debtor_account_id, 'IBAN')
 
212
        debtor_account_iban.text = my_company_iban
 
213
        debtor_agent = etree.SubElement(payment_info, 'DbtrAgt')
 
214
        debtor_agent_institution = etree.SubElement(debtor_agent, 'FinInstnId')
 
215
        debtor_agent_bic = etree.SubElement(debtor_agent_institution, bic_xml_tag)
 
216
        debtor_agent_bic.text = my_company_bic
 
217
        charge_bearer = etree.SubElement(payment_info, 'ChrgBr')
 
218
        charge_bearer.text = sepa_export.charge_bearer
 
219
 
 
220
        transactions_count = 0
 
221
        total_amount = 0.0
 
222
        amount_control_sum = 0.0
 
223
        # Iterate on payment orders
 
224
        for payment_order in sepa_export.payment_order_ids:
 
225
            total_amount = total_amount + payment_order.total
 
226
            # Iterate each payment lines
 
227
            for line in payment_order.line_ids:
 
228
                transactions_count += 1
 
229
                # C. Credit Transfer Transaction Info
 
230
                credit_transfer_transaction_info = etree.SubElement(payment_info, 'CdtTrfTxInf')
 
231
                payment_identification = etree.SubElement(credit_transfer_transaction_info, 'PmtId')
 
232
                instruction_identification = etree.SubElement(payment_identification, 'InstrId')
 
233
                instruction_identification.text = self._limit_size(cr, uid, line.communication, 35, context=context) #otherwise, we can reach the invoice fields via ml_inv_ref
 
234
                end2end_identification = etree.SubElement(payment_identification, 'EndToEndId')
 
235
                end2end_identification.text = self._limit_size(cr, uid, line.communication, 35, context=context)
 
236
                amount = etree.SubElement(credit_transfer_transaction_info, 'Amt')
 
237
                instructed_amount = etree.SubElement(amount, 'InstdAmt', Ccy=line.currency.name)
 
238
                instructed_amount.text = '%.2f' % line.amount_currency
 
239
                amount_control_sum += line.amount_currency
 
240
                creditor_agent = etree.SubElement(credit_transfer_transaction_info, 'CdtrAgt')
 
241
                creditor_agent_institution = etree.SubElement(creditor_agent, 'FinInstnId')
 
242
                creditor_agent_bic = etree.SubElement(creditor_agent_institution, bic_xml_tag)
 
243
                creditor_agent_bic.text = line.bank_id.bank.bic
 
244
                creditor = etree.SubElement(credit_transfer_transaction_info, 'Cdtr')
 
245
                creditor_name = etree.SubElement(creditor, 'Nm')
 
246
                creditor_name.text = self._limit_size(cr, uid, line.partner_id.name, name_maxsize, context=context)
 
247
# I don't think they want it
 
248
# If they want it, we need to implement full spec p26 appendix
 
249
#                creditor_address = etree.SubElement(creditor, 'PstlAdr')
 
250
#                creditor_street = etree.SubElement(creditor_address, 'AdrLine')
 
251
#                creditor_street.text = line.partner_id.address[0].street
 
252
#                creditor_city = etree.SubElement(creditor_address, 'AdrLine')
 
253
#                creditor_city.text = line.partner_id.address[0].city
 
254
#                creditor_country = etree.SubElement(creditor_address, 'Ctry')
 
255
#                creditor_country.text = line.partner_id.address[0].country_id.code
 
256
                creditor_account = etree.SubElement(credit_transfer_transaction_info, 'CdtrAcct')
 
257
                creditor_account_id = etree.SubElement(creditor_account, 'Id')
 
258
                creditor_account_iban = etree.SubElement(creditor_account_id, 'IBAN')
 
259
                creditor_account_iban.text = self._validate_iban(cr, uid, line.bank_id.iban, context=context)
 
260
                remittance_info = etree.SubElement(credit_transfer_transaction_info, 'RmtInf')
 
261
                # switch to Structured (Strdr) ? If we do it, beware that the format is not the same between pain 02 and pain 03
 
262
                remittance_info_unstructured = etree.SubElement(remittance_info, 'Ustrd')
 
263
                remittance_info_unstructured.text = self._limit_size(cr, uid, line.communication, 140, context=context)
 
264
 
 
265
        if pain_flavor in ['pain.001.001.03', 'pain.001.001.04']:
 
266
            nb_of_transactions_grphdr.text = nb_of_transactions_pmtinf.text = str(transactions_count)
 
267
            control_sum_grphdr.text = control_sum_pmtinf.text = '%.2f' % amount_control_sum
 
268
        else:
 
269
            nb_of_transactions_grphdr.text = str(transactions_count)
 
270
            control_sum_grphdr.text = '%.2f' % amount_control_sum
 
271
 
 
272
 
 
273
        xml_string = etree.tostring(root, pretty_print=True, encoding='UTF-8', xml_declaration=True)
 
274
        _logger.debug("Generated SEPA XML file below")
 
275
        _logger.debug(xml_string)
 
276
        official_pain_schema = etree.XMLSchema(etree.parse(tools.file_open('account_banking_sepa_credit_transfer/data/%s.xsd' % pain_flavor)))
 
277
 
 
278
        try:
 
279
            official_pain_schema.validate(root)
 
280
        except Exception, e:
 
281
            _logger.warning("The XML file is invalid against the XML Schema Definition")
 
282
            _logger.warning(xml_string)
 
283
            _logger.warning(e)
 
284
            raise osv.except_osv(_('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))
 
285
 
 
286
        # CREATE the banking.export.sepa record
 
287
        file_id = self.pool.get('banking.export.sepa').create(cr, uid,
 
288
            {
 
289
            'msg_identification': my_msg_identification,
 
290
            'batch_booking': sepa_export.batch_booking,
 
291
            'charge_bearer': sepa_export.charge_bearer,
 
292
            'prefered_exec_date': sepa_export.prefered_exec_date,
 
293
            'total_amount': total_amount,
 
294
            'nb_transactions': transactions_count,
 
295
            'file': base64.encodestring(xml_string),
 
296
            'payment_order_ids': [
 
297
                (6, 0, [x.id for x in sepa_export.payment_order_ids])
 
298
            ],
 
299
            }, context=context)
 
300
 
 
301
        self.write(cr, uid, ids, {
 
302
            'file_id': file_id,
 
303
            'state': 'finish',
 
304
            }, context=context)
 
305
 
 
306
        action = {
 
307
            'name': 'SEPA XML',
 
308
            'type': 'ir.actions.act_window',
 
309
            'view_type': 'form',
 
310
            'view_mode': 'form,tree',
 
311
            'res_model': self._name,
 
312
            'res_id': ids[0],
 
313
            'target': 'new',
 
314
            }
 
315
        return action
 
316
 
 
317
 
 
318
    def cancel_sepa(self, cr, uid, ids, context=None):
 
319
        '''
 
320
        Cancel the SEPA PAIN: just drop the file
 
321
        '''
 
322
        sepa_export = self.browse(cr, uid, ids[0], context=context)
 
323
        self.pool.get('banking.export.sepa').unlink(cr, uid, sepa_export.file_id.id, context=context)
 
324
        return {'type': 'ir.actions.act_window_close'}
 
325
 
 
326
 
 
327
    def save_sepa(self, cr, uid, ids, context=None):
 
328
        '''
 
329
        Save the SEPA PAIN: mark all payments in the file as 'sent'.
 
330
        '''
 
331
        sepa_export = self.browse(cr, uid, ids[0], context=context)
 
332
        sepa_file = self.pool.get('banking.export.sepa').write(cr, uid,
 
333
            sepa_export.file_id.id, {'state': 'sent'}, context=context)
 
334
        wf_service = netsvc.LocalService('workflow')
 
335
        for order in sepa_export.payment_order_ids:
 
336
            wf_service.trg_validate(uid, 'payment.order', order.id, 'sent', cr)
 
337
        return {'type': 'ir.actions.act_window_close'}
 
338
 
 
339
 
 
340
banking_export_sepa_wizard()
 
341