1
# -*- encoding: utf-8 -*-
2
##############################################################################
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>
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.
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.
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/>.
21
##############################################################################
24
from osv import osv, fields
26
from datetime import datetime, timedelta
27
from tools.translate import _
29
from lxml import etree
33
_logger = logging.getLogger(__name__)
36
class banking_export_sepa_wizard(osv.osv_memory):
37
_name = 'banking.export.sepa.wizard'
38
_description = 'Export SEPA Credit Transfer XML file'
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([
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',
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),
72
'charge_bearer': 'SLEV',
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]
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(' ', '')
90
raise osv.except_osv(_('Error :'), _("This IBAN is not valid : %s") % iban)
92
def create(self, cr, uid, vals, context=None):
93
payment_order_ids = context.get('active_ids', [])
95
'payment_order_ids': [[6, 0, payment_order_ids]],
97
return super(banking_export_sepa_wizard, self).create(cr, uid,
98
vals, context=context)
101
def create_sepa(self, cr, uid, ids, context=None):
103
Creates the SEPA Credit Transfer file. That's the important code !
105
payment_order_obj = self.pool.get('payment.order')
107
sepa_export = self.browse(cr, uid, ids[0], context=context)
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':
119
root_xml_tag = 'pain.001.001.02'
120
elif pain_flavor == 'pain.001.001.03':
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
129
root_xml_tag = 'CstmrCdtTrfInitn'
130
elif pain_flavor == 'pain.001.001.04':
131
bic_xml_tag = 'BICFI'
133
root_xml_tag = 'CstmrCdtTrfInitn'
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'
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
144
my_requested_exec_date = datetime.strftime(datetime.today() + timedelta(days=1), '%Y-%m-%d')
147
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
148
None: 'urn:iso:std:iso:20022:tech:xsd:%s' % pain_flavor,
151
root = etree.Element('Document', nsmap=pain_ns)
152
pain_root = etree.SubElement(root, root_xml_tag)
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)
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
220
transactions_count = 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)
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
269
nb_of_transactions_grphdr.text = str(transactions_count)
270
control_sum_grphdr.text = '%.2f' % amount_control_sum
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)))
279
official_pain_schema.validate(root)
281
_logger.warning("The XML file is invalid against the XML Schema Definition")
282
_logger.warning(xml_string)
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))
286
# CREATE the banking.export.sepa record
287
file_id = self.pool.get('banking.export.sepa').create(cr, uid,
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])
301
self.write(cr, uid, ids, {
308
'type': 'ir.actions.act_window',
310
'view_mode': 'form,tree',
311
'res_model': self._name,
318
def cancel_sepa(self, cr, uid, ids, context=None):
320
Cancel the SEPA PAIN: just drop the file
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'}
327
def save_sepa(self, cr, uid, ids, context=None):
329
Save the SEPA PAIN: mark all payments in the file as 'sent'.
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'}
340
banking_export_sepa_wizard()