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