1
# -*- encoding: utf-8 -*-
2
########################################################################
4
# Copyright (C) 2013 Acysos S.L.
5
# Copyright (C) 2010-2013 Akretion (http://www.akretion.com)
6
# @authors: Ignacio Ibeas <ignacio@acysos.com>
7
# @authors: Alexis de Lattre <alexis.delattre@akretion.com>
9
#This program is free software: you can redistribute it and/or modify
10
#it under the terms of the GNU General Public License as published by
11
#the Free Software Foundation, either version 3 of the License, or
12
#(at your option) any later version.
14
# This module is GPLv3 or newer and incompatible
15
# with OpenERP SA "AGPL + Private Use License"!
17
#This program is distributed in the hope that it will be useful,
18
#but WITHOUT ANY WARRANTY; without even the implied warranty of
19
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
#GNU General Public License for more details.
22
#You should have received a copy of the GNU General Public License
23
#along with this program. If not, see http://www.gnu.org/licenses.
24
########################################################################
26
from osv import osv, fields
29
from datetime import datetime, timedelta
30
from tools.translate import _
31
from tools.safe_eval import safe_eval
33
from lxml import etree
36
from unidecode import unidecode
38
_logger = logging.getLogger(__name__)
40
class sepa_pain_export(osv.osv):
41
_name = 'sepa.pain.export'
42
_description = 'Export to SEPA XML format'
45
def _validate_iban(self, cr, uid, bank, context=None):
46
'''if IBAN is valid, returns IBAN
47
if IBAN is NOT valid, raises an error message'''
48
partner_bank_obj = self.pool.get('res.partner.bank')
50
if partner_bank_obj.check_iban(cr, uid, [bank.id], context=context):
51
return iban.replace(' ', '')
54
_('Error:'), _("This IBAN is not valid : %s") % iban)
57
self, cr, uid, field_name, field_value, eval_ctx, max_size=0,
59
'''This function is designed to be inherited !'''
60
assert isinstance(eval_ctx, dict), 'eval_ctx must contain a dict'
62
# SEPA uses XML ; XML = UTF-8 ; UTF-8 = support for all characters
63
# But we are dealing with banks...
64
# and many banks don't want non-ASCCI characters !
65
# cf section 1.4 "Character set" of the SEPA Credit Transfer
66
# Scheme Customer-to-bank guidelines
67
value = unidecode(safe_eval(field_value, eval_ctx))
69
line = eval_ctx.get('line')
73
_("Cannot compute the '%s' of the Payment Line with Invoice Reference '%s'.")
75
self.pool['account.invoice'].name_get(
76
cr, uid, [line.ml_inv_ref.id],
77
context=context)[0][1]))
81
_("Cannot compute the '%s'.") % field_name)
82
if not isinstance(value, (str, unicode)):
84
_('Field type error:'),
85
_('''The type of the field '%s' is %s.
86
It should be a string or unicode.''')
87
% (field_name, type(value)))
91
_("The '%s' is empty or 0. It should have a non-null value.")
93
if max_size and len(value) > max_size:
94
value = value[0:max_size]
97
def _validate_xml(self, cr, uid, xml_string, config):
98
xsd_etree_obj = etree.parse(
100
'sepa_pain/data/%s.xsd'
101
% config['pain_flavor']))
102
official_pain_schema = etree.XMLSchema(xsd_etree_obj)
105
root_to_validate = etree.fromstring(xml_string)
106
official_pain_schema.assertValid(root_to_validate)
109
"The XML file is invalid against the XML Schema Definition")
110
_logger.warning(xml_string)
112
raise osv.except_osv(
114
_('''The generated XML file is not valid against the official
115
XML Schema Definition. The generated XML file and the full
116
error have been written in the server logs. Here is the
117
error, which may give you an idea on the cause of the
118
problem : %s''') % str(e))
121
def prepare_config(self, cr, uid, payment_order,
122
pain_flavor, batch_booking,
123
prefered_exec_date, charge_bearer, context):
126
if pain_flavor == 'pain.001.001.02':
127
config['bic_xml_tag'] = 'BIC'
128
config['name_maxsize'] = 70
129
config['root_xml_tag'] = 'CstmrCdtTrfInitn'
130
config['company_tag'] = 'Db'
131
config['partner_tag'] = 'Cd'
132
config['line_tag'] = 'CdtTrfTxInf'
133
elif pain_flavor == 'pain.001.001.03':
134
config['bic_xml_tag'] = 'BIC'
135
# size 70 -> 140 for <Nm> with pain.001.001.03
136
# BUT the European Payment Council, in the document
137
# "SEPA Credit Transfer Scheme Customer-to-bank Implementation guidelines" v6.0
138
# available on http://www.europeanpaymentscouncil.eu/knowledge_bank.cfm
139
# says that 'Nm' should be limited to 70
140
# so we follow the "European Payment Council" and we put 70 and not 140
141
config['name_maxsize'] = 70
142
config['root_xml_tag'] = 'CstmrCdtTrfInitn'
143
config['company_tag'] = 'Db'
144
config['partner_tag'] = 'Cd'
145
config['line_tag'] = 'CdtTrfTxInf'
146
elif pain_flavor == 'pain.001.001.04':
147
config['bic_xml_tag'] = 'BICFI'
148
config['name_maxsize'] = 70
149
config['root_xml_tag'] = 'CstmrCdtTrfInitn'
150
config['company_tag'] = 'Db'
151
config['partner_tag'] = 'Cd'
152
config['line_tag'] = 'CdtTrfTxInf'
153
elif pain_flavor == 'pain.001.001.05':
154
config['bic_xml_tag'] = 'BICFI'
155
config['name_maxsize'] = 140
156
config['root_xml_tag'] = 'CstmrCdtTrfInitn'
157
config['company_tag'] = 'Db'
158
config['partner_tag'] = 'Cd'
159
config['line_tag'] = 'CdtTrfTxInf'
160
elif pain_flavor == 'pain.008.001.02':
161
config['bic_xml_tag'] = 'BIC'
162
config['name_maxsize'] = 70
163
config['root_xml_tag'] = 'CstmrDrctDbtInitn'
164
config['company_tag'] = 'Cd'
165
config['partner_tag'] = 'Db'
166
config['line_tag'] = 'DrctDbtTxInf'
168
raise osv.except_osv(
170
_("""Payment Type Code '%s' is not supported.
171
Type Codes supported for SEPA are
172
'pain.001.001.02', 'pain.001.001.03', 'pain.001.001.04',
173
'pain.001.001.05' and 'pain.008.001.02.""")
175
config['pain_flavor'] = pain_flavor
176
config['batch_booking'] = batch_booking
177
if prefered_exec_date:
178
config['prefered_exec_date'] = config['prefered_exec_date']
180
config['prefered_exec_date'] = datetime.strftime(datetime.today() +
181
timedelta(days=1), '%Y-%m-%d')
182
config['charge_bearer'] = charge_bearer
183
config['company_name'] = self._prepare_field(
184
cr, uid, 'Company Name',
185
'payment_order.mode.bank_id.partner_id.name',
186
{'payment_order': payment_order}, config['name_maxsize'],
191
def get_GrpHdr(self,cr,uid, root, payment_order,config,context):
193
pain_root = root.find(config['root_xml_tag'])
194
group_header_1_0 = etree.SubElement(pain_root, 'GrpHdr')
195
message_identification_1_1 = etree.SubElement(
196
group_header_1_0, 'MsgId')
197
message_identification_1_1.text = self._prepare_field(
198
cr, uid, 'Message Identification',
199
'payment_order.reference',
200
{'payment_order': payment_order}, 35, context=context)
201
creation_date_time_1_2 = etree.SubElement(group_header_1_0, 'CreDtTm')
202
creation_date_time_1_2.text = datetime.strftime(
203
datetime.today(), '%Y-%m-%dT%H:%M:%S')
204
if config['pain_flavor'] == 'pain.001.001.02':
205
# batch_booking is in "Group header" with pain.001.001.02
206
# and in "Payment info" in pain.001.001.03/04
207
batch_booking = etree.SubElement(group_header_1_0, 'BtchBookg')
208
batch_booking.text = str(sepa_export.batch_booking).lower()
209
nb_of_transactions_1_6 = etree.SubElement(
210
group_header_1_0, 'NbOfTxs')
211
control_sum_1_7 = etree.SubElement(group_header_1_0, 'CtrlSum')
212
# Grpg removed in pain.001.001.03
213
if config['pain_flavor'] == 'pain.001.001.02':
214
grouping = etree.SubElement(group_header_1_0, 'Grpg')
215
grouping.text = 'GRPD'
216
initiating_party_1_8 = etree.SubElement(group_header_1_0, 'InitgPty')
217
initiating_party_name = etree.SubElement(initiating_party_1_8, 'Nm')
218
initiating_party_name.text = config['company_name']
219
if payment_order.mode.bank_id.partner_id.vat:
220
initiating_party_id = etree.SubElement(initiating_party_1_8,'Id')
221
initiating_party_orgid = etree.SubElement(initiating_party_id,
223
initiating_party_othr = etree.SubElement(initiating_party_orgid,
225
initiating_party_othr_id = etree.SubElement(initiating_party_othr,
227
initiating_party_othr_id.text = \
228
payment_order.mode.bank_id.partner_id.vat
232
def get_PmtInf_basic(self,cr,uid, root, payment_order,config,context):
234
pain_root = root.find(config['root_xml_tag'])
235
payment_info_2_0 = etree.SubElement(pain_root, 'PmtInf')
236
payment_info_identification_2_1 = etree.SubElement(
237
payment_info_2_0, 'PmtInfId')
238
payment_info_identification_2_1.text = self._prepare_field(
239
cr, uid, 'Payment Information Identification',
240
"payment_order.reference",
241
{'payment_order': payment_order}, 35, context=context)
242
payment_method_2_2 = etree.SubElement(payment_info_2_0, 'PmtMtd')
243
if config['pain_flavor'] in ['pain.001.001.02','pain.001.001.03',
244
'pain.001.001.04', 'pain.001.001.05']:
245
payment_method_2_2.text = 'TRF'
246
elif config['pain_flavor'] in ['pain.008.001.02']:
247
payment_method_2_2.text = 'DD'
248
if config['pain_flavor'] in [
249
'pain.001.001.03', 'pain.001.001.04', 'pain.001.001.05']:
250
# batch_booking is in "Group header" with pain.001.001.02
251
# and in "Payment info" in pain.001.001.03/04
252
batch_booking_2_3 = etree.SubElement(payment_info_2_0, 'BtchBookg')
253
batch_booking_2_3.text = str(config['batch_booking']).lower()
254
# It may seem surprising, but the
255
# "SEPA Credit Transfer Scheme Customer-to-bank Implementation
256
# guidelines" v6.0 says that control sum and nb_of_transactions
257
# should be present at both "group header" level and "payment info"
258
# level. This seems to be confirmed by the tests carried out at
259
# BNP Paribas in PAIN v001.001.03
260
if config['pain_flavor'] in [
261
'pain.001.001.03', 'pain.001.001.04', 'pain.001.001.05']:
262
nb_of_transactions_2_4 = etree.SubElement(
263
payment_info_2_0, 'NbOfTxs')
264
control_sum_2_5 = etree.SubElement(payment_info_2_0, 'CtrlSum')
265
payment_type_info_2_6 = etree.SubElement(payment_info_2_0, 'PmtTpInf')
266
service_level_2_8 = etree.SubElement(payment_type_info_2_6, 'SvcLvl')
267
service_level_code_2_9 = etree.SubElement(service_level_2_8, 'Cd')
268
service_level_code_2_9.text = 'SEPA'
269
if config['pain_flavor'] in ['pain.001.001.02','pain.001.001.03',
270
'pain.001.001.04', 'pain.001.001.05']:
271
requested_exec_date_2_17 = etree.SubElement(
272
payment_info_2_0, 'ReqdExctnDt')
273
elif config['pain_flavor'] in ['pain.008.001.02']:
274
requested_exec_date_2_17 = etree.SubElement(
275
payment_info_2_0, 'ReqdColltnDt')
276
requested_exec_date_2_17.text = config['prefered_exec_date']
277
company_2_19 = etree.SubElement(payment_info_2_0, config['company_tag']+'tr')
278
company_name = etree.SubElement(company_2_19, 'Nm')
279
company_name.text = config['company_name']
280
company_account_2_20 = etree.SubElement(payment_info_2_0,
281
config['company_tag']+'trAcct')
282
company_account_id = etree.SubElement(company_account_2_20, 'Id')
283
company_account_iban = etree.SubElement(company_account_id, 'IBAN')
285
if (self._validate_iban(cr,uid,payment_order.mode.bank_id,
287
company_account_iban.text = self._prepare_field(
288
cr, uid, 'Company IBAN',
289
'payment_order.mode.bank_id.iban',
290
{'payment_order': payment_order},
291
context=context).replace(' ', '')
293
raise osv.except_osv(
295
_("""Company IBAN '%s' is not valid.""")
296
% payment_order.mode.bank_id.iban)
297
company_agent_2_21 = etree.SubElement(payment_info_2_0,
298
config['company_tag']+'trAgt')
299
company_agent_institution = etree.SubElement(
300
company_agent_2_21, 'FinInstnId')
301
company_agent_bic = etree.SubElement(
302
company_agent_institution, config['bic_xml_tag'])
303
# TODO validate BIC with pattern
304
# [A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}
305
# because OpenERP doesn't have a constraint on BIC
306
company_agent_bic.text = self._prepare_field(
307
cr, uid, 'Company BIC',
308
'payment_order.mode.bank_id.bank.bic',
309
{'payment_order': payment_order}, context=context)
310
charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr')
311
charge_bearer_2_24.text = config['charge_bearer']
315
def get_PmtInf_lines(self,cr,uid, root, payment_order,config,context):
317
transactions_count = 0
319
amount_control_sum = 0.0
320
total_amount = total_amount + payment_order.total
322
payment_info_2_0 = root.find(config['root_xml_tag']).find('PmtInf')
323
# Iterate each payment lines
324
for line in payment_order.line_ids:
325
transactions_count += 1
326
transaction_info_2_27 = etree.SubElement(
327
payment_info_2_0, config['line_tag'])
328
payment_identification_2_28 = etree.SubElement(
329
transaction_info_2_27, 'PmtId')
330
end2end_identification_2_30 = etree.SubElement(
331
payment_identification_2_28, 'EndToEndId')
332
end2end_identification_2_30.text = self._prepare_field(
333
cr, uid, 'End to End Identification', 'line.name',
334
{'line': line}, 35, context=context)
335
currency_name = self._prepare_field(
336
cr, uid, 'Currency Code', 'line.currency.name',
337
{'line': line}, 3, context=context)
339
if config['pain_flavor'] in ['pain.001.001.02','pain.001.001.03',
340
'pain.001.001.04', 'pain.001.001.05']:
341
amount_2_42 = etree.SubElement(
342
transaction_info_2_27, 'Amt')
343
instructed_amount_2_43 = etree.SubElement(
344
amount_2_42, 'InstdAmt', Ccy=currency_name)
345
elif config['pain_flavor'] in ['pain.008.001.02']:
346
instructed_amount_2_43 = etree.SubElement(
347
transaction_info_2_27,
348
'InstdAmt', Ccy=currency_name)
350
instructed_amount_2_43.text = '%.2f' % line.amount_currency
351
amount_control_sum += line.amount_currency
352
partner_agent_2_77 = etree.SubElement(
353
transaction_info_2_27, config['partner_tag']+'trAgt')
354
partner_agent_institution = etree.SubElement(
355
partner_agent_2_77, 'FinInstnId')
357
raise osv.except_osv(
359
_("Missing Bank Account on invoice '%s' (payment order line reference '%s').")
360
% (line.ml_inv_ref.number, line.name))
361
partner_agent_bic = etree.SubElement(
362
partner_agent_institution, config['bic_xml_tag'])
363
partner_agent_bic.text = self._prepare_field(
364
cr, uid, 'Customer BIC', 'line.bank_id.bank.bic',
365
{'line': line}, context=context)
366
partner_2_79 = etree.SubElement(
367
transaction_info_2_27, config['partner_tag']+'tr')
368
partner_name = etree.SubElement(partner_2_79, 'Nm')
369
partner_name.text = self._prepare_field(
370
cr, uid, 'Customer Name', 'line.partner_id.name',
371
{'line': line}, config['name_maxsize'], context=context)
372
partner_account_2_80 = etree.SubElement(
373
transaction_info_2_27, config['partner_tag']+'trAcct')
374
partner_account_id = etree.SubElement(
375
partner_account_2_80, 'Id')
376
partner_account_iban = etree.SubElement(
377
partner_account_id, 'IBAN')
379
if (self._validate_iban(cr, uid, line.bank_id,context)):
380
partner_account_iban.text = self._prepare_field(
381
cr, uid, 'Customer IBAN',
382
'line.bank_id.iban', {'line': line},
383
context=context).replace(' ', '')
385
raise osv.except_osv(
387
_("""Partner IBAN '%s' is not valid.""")
389
remittance_info_2_91 = etree.SubElement(
390
transaction_info_2_27, 'RmtInf')
391
# switch to Structured (Strdr) ?
392
# If we do it, beware that the format is not the same
393
# between pain 02 and pain 03
394
remittance_info_unstructured_2_99 = etree.SubElement(
395
remittance_info_2_91, 'Ustrd')
396
remittance_info_unstructured_2_99.text = self._prepare_field(
397
cr, uid, 'Remittance Information', 'line.communication',
398
{'line': line}, 140, context=context)
400
return root, transactions_count, amount_control_sum, total_amount
402
def create_sepa(self,cr,uid,payment_order, config,context):
408
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
409
None: 'urn:iso:std:iso:20022:tech:xsd:%s' % config['pain_flavor'],
412
root = etree.Element('Document', nsmap=pain_ns)
413
pain_root = etree.SubElement(root, config['root_xml_tag'])
415
root = self.get_GrpHdr(cr, uid, root, payment_order,
418
root = self.get_PmtInf_basic(cr, uid, root, payment_order,
421
root, transactions_count, amount_control_sum, total_amount = \
422
self.get_PmtInf_lines(cr, uid, root, payment_order,
425
nb_of_transactions_1_6 = pain_root.find('GrpHdr').find('NbOfTxs')
426
nb_of_transactions_2_4 = pain_root.find('PmtInf').find('NbOfTxs')
427
control_sum_1_7 = pain_root.find('GrpHdr').find('CtrlSum')
428
control_sum_2_5 = pain_root.find('PmtInf').find('CtrlSum')
429
if config['pain_flavor'] in [
430
'pain.001.001.03', 'pain.001.001.04', 'pain.001.001.05']:
431
nb_of_transactions_1_6.text = nb_of_transactions_2_4.text = \
432
str(transactions_count)
433
control_sum_1_7.text = control_sum_2_5.text = \
434
'%.2f' % amount_control_sum
436
nb_of_transactions_1_6.text = str(transactions_count)
437
control_sum_1_7.text = '%.2f' % amount_control_sum
439
return root, transactions_count, total_amount
441
def create_file(self,cr,uid,root,config,payment_order,context):
443
xml_string = etree.tostring(
444
root, pretty_print=True, encoding='UTF-8', xml_declaration=True)
446
"Generated SEPA Credit Transfer XML file in format %s below"
447
% config['pain_flavor'])
448
_logger.debug(xml_string)
449
self._validate_xml(cr, uid, xml_string, config)
452
## Generate the file and save as attachment
453
file = base64.encodestring(xml_string)
455
file_name = _("SEPA_report_%s_%s.xml") % (time.strftime(_("%Y-%m-%d")),
456
config['pain_flavor'])
459
obj_attachment = self.pool.get('ir.attachment')
460
attachment_ids = obj_attachment.search(cr, uid,
461
[('name', '=', file_name),
462
('res_model', '=', 'payment.order'),
463
('res_id', '=' , payment_order.id)])
465
if len(attachment_ids):
466
obj_attachment.unlink(cr, uid, attachment_ids)
468
attach_id = obj_attachment.create(cr, uid, {
471
'datas_fname' : file_name,
472
'res_model' : 'payment.order',
473
'res_id' : payment_order.id
b'\\ No newline at end of file'