From f42492897caef18d1c022adb4e5fcf489d07136f Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 6 Nov 2013 16:42:24 +0100 Subject: [PATCH] Add support for pain.001.001.05 Fix dependencies (I need the field "payment_order_type" which is in account_banking_payment) Only use ASCII characters in the XML file (that's how banks use XML !) Use the payment order reference as the Message Identification of the XML file. Use the payment line sequence as the "End to End Identification" Remove the Instruction Identification ('InstrId') because it is not a required field (only the End to End identification is required) Rename variables in the function create_sepa() to include the number of each field as used in the EPC guidelines Reduce the number of flake8 warnings Make code more modular : - self.pool.get('banking.export.sepa').create() has a _prepare fonction - most of the content of the XML file can be inherited via _prepare_field() --- .../__openerp__.py | 9 +- .../account_banking_sepa.py | 42 +- .../account_banking_sepa_view.xml | 3 +- .../data/pain.001.001.05.xsd | 931 ++++++++++++++++++ .../data/payment_type_sepa_sct.xml | 17 +- .../wizard/export_sepa.py | 472 +++++---- .../wizard/export_sepa_view.xml | 9 +- 7 files changed, 1269 insertions(+), 214 deletions(-) create mode 100644 account_banking_sepa_credit_transfer/data/pain.001.001.05.xsd diff --git a/account_banking_sepa_credit_transfer/__openerp__.py b/account_banking_sepa_credit_transfer/__openerp__.py index 751058ea2..5f310feec 100644 --- a/account_banking_sepa_credit_transfer/__openerp__.py +++ b/account_banking_sepa_credit_transfer/__openerp__.py @@ -21,12 +21,15 @@ { 'name': 'Account Banking SEPA Credit Transfer', 'summary': 'Create SEPA XML files for Credit Transfers', - 'version': '0.1', + 'version': '0.2', 'license': 'AGPL-3', 'author': 'Akretion', 'website': 'http://www.akretion.com', 'category': 'Banking addons', - 'depends': ['account_banking_payment_export'], + 'depends': ['account_banking_payment'], + 'external_dependencies': { + 'python': ['unidecode', 'lxml'], + }, 'data': [ 'account_banking_sepa_view.xml', 'wizard/export_sepa_view.xml', @@ -36,7 +39,7 @@ 'description': ''' Module to export 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 Credit Transfer (SCT), more specifically PAIN versions 001.001.02, 001.001.03 and 001.001.04. It is part of the ISO 20022 standard, available on http://www.iso20022.org. +SEPA PAIN (PAyment INitiation) is the new european standard for Customer-to-Bank payment instructions. This module implements SEPA Credit Transfer (SCT), more specifically PAIN versions 001.001.02, 001.001.03, 001.001.04 and 001.001.05. It is part of the ISO 20022 standard, available on http://www.iso20022.org. The Implementation Guidelines for SEPA Credit Transfer published by the European Payments Council (http://http://www.europeanpaymentscouncil.eu) use PAIN version 001.001.03, so it's probably the version of PAIN that you should try first. diff --git a/account_banking_sepa_credit_transfer/account_banking_sepa.py b/account_banking_sepa_credit_transfer/account_banking_sepa.py index 432b59267..cd05413cf 100644 --- a/account_banking_sepa_credit_transfer/account_banking_sepa.py +++ b/account_banking_sepa_credit_transfer/account_banking_sepa.py @@ -21,18 +21,24 @@ from openerp.osv import orm, fields from openerp.addons.decimal_precision import decimal_precision as dp +from unidecode import unidecode class banking_export_sepa(orm.Model): '''SEPA export''' _name = 'banking.export.sepa' _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] = 'sepa_' + (sepa_file.msg_identification or '') + '.xml' + ref = sepa_file.payment_order_ids[0].reference + if ref: + label = ref.replace('/', '-') + else: + label = 'error' + res[sepa_file.id] = 'sct_%s.xml' % label return res _columns = { @@ -40,32 +46,34 @@ class banking_export_sepa(orm.Model): 'payment.order', 'account_payment_order_sepa_rel', 'banking_export_sepa_id', 'account_order_id', - 'Payment orders', + 'Payment Orders', readonly=True), - 'prefered_exec_date': fields.date('Prefered execution date', readonly=True), - 'nb_transactions': fields.integer('Number of transactions', readonly=True), - 'total_amount': fields.float('Total amount', + 'prefered_exec_date': fields.date( + 'Prefered Execution 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, + 'batch_booking': fields.boolean( + 'Batch Booking', readonly=True, 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."), 'charge_bearer': fields.selection([ ('SHAR', 'Shared'), ('CRED', 'Borne by creditor'), ('DEBT', 'Borne by debtor'), ('SLEV', 'Following service level'), - ], 'Charge bearer', readonly=True, + ], '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', + '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, string='Filename', 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'), + ('draft', 'Draft'), + ('sent', 'Sent'), + ('done', 'Reconciled'), ], 'State', readonly=True), } diff --git a/account_banking_sepa_credit_transfer/account_banking_sepa_view.xml b/account_banking_sepa_credit_transfer/account_banking_sepa_view.xml index 1fdab318f..c4bd1aa8a 100644 --- a/account_banking_sepa_credit_transfer/account_banking_sepa_view.xml +++ b/account_banking_sepa_credit_transfer/account_banking_sepa_view.xml @@ -14,7 +14,6 @@
- @@ -47,7 +46,7 @@ banking.export.sepa - + diff --git a/account_banking_sepa_credit_transfer/data/pain.001.001.05.xsd b/account_banking_sepa_credit_transfer/data/pain.001.001.05.xsd new file mode 100644 index 000000000..18bd0cc7c --- /dev/null +++ b/account_banking_sepa_credit_transfer/data/pain.001.001.05.xsd @@ -0,0 +1,931 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/account_banking_sepa_credit_transfer/data/payment_type_sepa_sct.xml b/account_banking_sepa_credit_transfer/data/payment_type_sepa_sct.xml index c5d80fec3..35dad3afc 100644 --- a/account_banking_sepa_credit_transfer/data/payment_type_sepa_sct.xml +++ b/account_banking_sepa_credit_transfer/data/payment_type_sepa_sct.xml @@ -4,20 +4,29 @@ + + SEPA Credit Transfer v05 + pain.001.001.05 + + + payment + + SEPA Credit Transfer v04 pain.001.001.04 + eval="[(6,0,[ref('base_iban.bank_iban')])]" /> payment - SEPA Credit Transfer v03 + SEPA Credit Transfer v03 (recommended) pain.001.001.03 + eval="[(6,0,[ref('base_iban.bank_iban')])]" /> payment @@ -26,7 +35,7 @@ SEPA Credit Transfer v02 pain.001.001.02 + eval="[(6,0,[ref('base_iban.bank_iban')])]" /> payment diff --git a/account_banking_sepa_credit_transfer/wizard/export_sepa.py b/account_banking_sepa_credit_transfer/wizard/export_sepa.py index 6a7850535..1a9d79c2d 100644 --- a/account_banking_sepa_credit_transfer/wizard/export_sepa.py +++ b/account_banking_sepa_credit_transfer/wizard/export_sepa.py @@ -23,11 +23,13 @@ from openerp.osv import orm, fields import base64 -from datetime import datetime, timedelta +from datetime import datetime from openerp.tools.translate import _ +from openerp.tools.safe_eval import safe_eval from openerp import tools, netsvc from lxml import etree import logging +from unidecode import unidecode _logger = logging.getLogger(__name__) @@ -35,36 +37,41 @@ _logger = logging.getLogger(__name__) class banking_export_sepa_wizard(orm.TransientModel): _name = 'banking.export.sepa.wizard' _description = 'Export SEPA Credit Transfer 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', + 'state': fields.selection([ + ('create', 'Create'), + ('finish', 'Finish'), + ], 'State', readonly=True), + '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."), - 'prefered_exec_date': fields.date('Prefered execution date', + 'prefered_exec_date': fields.date( + 'Prefered Execution Date', 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.'), 'charge_bearer': fields.selection([ ('SHAR', 'Shared'), ('CRED', 'Borne by creditor'), ('DEBT', 'Borne by debtor'), ('SLEV', 'Following service level'), - ], 'Charge bearer', required=True, + ], '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.sepa', 'SEPA XML file', readonly=True), - 'file': fields.related('file_id', 'file', string="File", type='binary', + '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), - 'filename': fields.related('file_id', 'filename', string="Filename", - type='char', size=256, readonly=True), - 'payment_order_ids': fields.many2many('payment.order', - 'wiz_sepa_payorders_rel', 'wizard_id', 'payment_order_id', - 'Payment orders', readonly=True), + 'file_id': fields.many2one( + 'banking.export.sepa', 'SEPA 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_sepa_payorders_rel', 'wizard_id', + 'payment_order_id', 'Payment Orders', readonly=True), } _defaults = { @@ -72,13 +79,6 @@ class banking_export_sepa_wizard(orm.TransientModel): 'state': 'create', } - - def _limit_size(self, cr, uid, field, max_size, context=None): - '''Limit size of strings to respect the PAIN standard''' - max_size = int(max_size) - return field[0:max_size] - - def _validate_iban(self, cr, uid, iban, context=None): '''if IBAN is valid, returns IBAN if IBAN is NOT valid, raises an error message''' @@ -86,29 +86,97 @@ class banking_export_sepa_wizard(orm.TransientModel): 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) + 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_sepa_wizard, self).create(cr, uid, - vals, context=context) + return super(banking_export_sepa_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): + '''This function is designed to be inherited !''' + eval_ctx = { + 'sepa_export': sepa_export, + 'line': line, + } + try: + # SEPA uses XML ; XML = UTF-8 ; UTF-8 = support for all characters + # But we are dealing with banks... + # and many banks don't want non-ASCCI characters ! + # cf section 1.4 "Character set" of the SEPA Credit Transfer + # Scheme Customer-to-bank guidelines + value = unidecode(safe_eval(field_value, eval_ctx)) + 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 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: + value = value[0:max_size] + return value + + def _prepare_export_sepa( + self, cr, uid, sepa_export, total_amount, transactions_count, + xml_string, context=None): + return { + 'batch_booking': sepa_export.batch_booking, + 'charge_bearer': sepa_export.charge_bearer, + 'prefered_exec_date': sepa_export.prefered_exec_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]) + ], + } + + def _validate_xml(self, cr, uid, xml_string, pain_flavor): + xsd_etree_obj = etree.parse( + tools.file_open( + 'account_banking_sepa_credit_transfer/data/%s.xsd' + % pain_flavor)) + official_pain_schema = etree.XMLSchema(xsd_etree_obj) + + try: + 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)) + return True def create_sepa(self, cr, uid, ids, context=None): ''' Creates the SEPA Credit Transfer file. That's the important code ! ''' sepa_export = self.browse(cr, uid, ids[0], context=context) - - my_company_name = sepa_export.payment_order_ids[0].mode.bank_id.partner_id.name - my_company_iban = self._validate_iban(cr, uid, sepa_export.payment_order_ids[0].mode.bank_id.acc_number, context=context) - my_company_bic = sepa_export.payment_order_ids[0].mode.bank_id.bank.bic - #my_company_country_code = sepa_export.payment_order_ids[0].mode.bank_id.partner_id.address[0].country_id.code - #my_company_city = sepa_export.payment_order_ids[0].mode.bank_id.partner_id.address[0].city - #my_company_street1 = sepa_export.payment_order_ids[0].mode.bank_id.partner_id.address[0].street pain_flavor = sepa_export.payment_order_ids[0].mode.type.code if pain_flavor == 'pain.001.001.02': bic_xml_tag = 'BIC' @@ -118,23 +186,28 @@ class banking_export_sepa_wizard(orm.TransientModel): bic_xml_tag = 'BIC' # size 70 -> 140 for with pain.001.001.03 # BUT the European Payment Council, in the document - # "SEPA Credit Transfer Scheme Customer-to-bank Implementation guidelines" v6.0 - # available on http://www.europeanpaymentscouncil.eu/knowledge_bank.cfm + # "SEPA Credit Transfer Scheme Customer-to-bank + # Implementation guidelines" v6.0 available on + # http://www.europeanpaymentscouncil.eu/knowledge_bank.cfm # says that 'Nm' should be limited to 70 - # so we follow the "European Payment Council" and we put 70 and not 140 + # so we follow the "European Payment Council" + # and we put 70 and not 140 name_maxsize = 70 root_xml_tag = 'CstmrCdtTrfInitn' elif pain_flavor == 'pain.001.001.04': bic_xml_tag = 'BICFI' name_maxsize = 140 root_xml_tag = 'CstmrCdtTrfInitn' + elif pain_flavor == 'pain.001.001.05': + bic_xml_tag = 'BICFI' + name_maxsize = 140 + root_xml_tag = 'CstmrCdtTrfInitn' + else: - raise orm.except_orm(_('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) - if sepa_export.batch_booking: - my_batch_booking = 'true' - else: - my_batch_booking = 'false' - my_msg_identification = sepa_export.msg_identification + raise orm.except_orm( + _('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', 'pain.001.001.04' and 'pain.001.001.05'.") + % pain_flavor) if sepa_export.prefered_exec_date: my_requested_exec_date = sepa_export.prefered_exec_date else: @@ -148,73 +221,99 @@ class banking_export_sepa_wizard(orm.TransientModel): 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].mode.bank_id.partner_id.name', + name_maxsize, sepa_export=sepa_export, context=context) + # A. Group header - group_header = etree.SubElement(pain_root, 'GrpHdr') - message_identification = etree.SubElement(group_header, 'MsgId') - message_identification.text = self._limit_size(cr, uid, my_msg_identification, 35, context=context) - creation_date_time = etree.SubElement(group_header, 'CreDtTm') - creation_date_time.text = datetime.strftime(datetime.today(), '%Y-%m-%dT%H:%M:%S') + 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 = self._prepare_field( + cr, uid, 'Message Identification', + 'sepa_export.payment_order_ids[0].reference', 35, + sepa_export=sepa_export, context=context) + 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') if pain_flavor == 'pain.001.001.02': # batch_booking is in "Group header" with pain.001.001.02 # and in "Payment info" in pain.001.001.03/04 - batch_booking = etree.SubElement(group_header, 'BtchBookg') - batch_booking.text = my_batch_booking - nb_of_transactions_grphdr = etree.SubElement(group_header, 'NbOfTxs') - control_sum_grphdr = etree.SubElement(group_header, 'CtrlSum') + batch_booking = etree.SubElement(group_header_1_0, 'BtchBookg') + batch_booking.text = str(sepa_export.batch_booking).lower() + nb_of_transactions_1_6 = etree.SubElement( + group_header_1_0, 'NbOfTxs') + control_sum_1_7 = etree.SubElement(group_header_1_0, 'CtrlSum') # Grpg removed in pain.001.001.03 if pain_flavor == 'pain.001.001.02': - grouping = etree.SubElement(group_header, 'Grpg') + grouping = etree.SubElement(group_header_1_0, 'Grpg') grouping.text = 'GRPD' - initiating_party = etree.SubElement(group_header, 'InitgPty') - initiating_party_name = etree.SubElement(initiating_party, 'Nm') - initiating_party_name.text = self._limit_size(cr, uid, my_company_name, name_maxsize, context=context) + 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 = etree.SubElement(pain_root, 'PmtInf') - payment_info_identification = etree.SubElement(payment_info, 'PmtInfId') - payment_info_identification.text = self._limit_size(cr, uid, my_msg_identification, 35, context=context) - payment_method = etree.SubElement(payment_info, 'PmtMtd') - payment_method.text = 'TRF' - if pain_flavor in ['pain.001.001.03', 'pain.001.001.04']: + 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 = self._prepare_field( + cr, uid, 'Payment Information Identification', + "sepa_export.payment_order_ids[0].reference", + 35, sepa_export=sepa_export, context=context) + payment_method_2_2 = etree.SubElement(payment_info_2_0, 'PmtMtd') + payment_method_2_2.text = 'TRF' + if pain_flavor in [ + 'pain.001.001.03', 'pain.001.001.04', 'pain.001.001.05']: # batch_booking is in "Group header" with pain.001.001.02 # and in "Payment info" in pain.001.001.03/04 - batch_booking = etree.SubElement(payment_info, 'BtchBookg') - batch_booking.text = my_batch_booking + 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 Credit Transfer 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 - # This seems to be confirmed by the tests carried out at + # "SEPA Credit Transfer 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. This seems to be confirmed by the tests carried out at # BNP Paribas in PAIN v001.001.03 - if pain_flavor in ['pain.001.001.03', 'pain.001.001.04']: - nb_of_transactions_pmtinf = etree.SubElement(payment_info, 'NbOfTxs') - control_sum_pmtinf = etree.SubElement(payment_info, 'CtrlSum') - payment_type_info = etree.SubElement(payment_info, 'PmtTpInf') - service_level = etree.SubElement(payment_type_info, 'SvcLvl') - service_level_code = etree.SubElement(service_level, 'Cd') - service_level_code.text = 'SEPA' - requested_exec_date = etree.SubElement(payment_info, 'ReqdExctnDt') - requested_exec_date.text = my_requested_exec_date - debtor = etree.SubElement(payment_info, 'Dbtr') - debtor_name = etree.SubElement(debtor, 'Nm') - debtor_name.text = self._limit_size(cr, uid, my_company_name, name_maxsize, context=context) -# debtor_address = etree.SubElement(debtor, 'PstlAdr') -# debtor_street = etree.SubElement(debtor_address, 'AdrLine') -# debtor_street.text = my_company_street1 -# debtor_city = etree.SubElement(debtor_address, 'AdrLine') -# debtor_city.text = my_company_city -# debtor_country = etree.SubElement(debtor_address, 'Ctry') -# debtor_country.text = my_company_country_code - debtor_account = etree.SubElement(payment_info, 'DbtrAcct') - debtor_account_id = etree.SubElement(debtor_account, 'Id') + if pain_flavor in [ + 'pain.001.001.03', 'pain.001.001.04', 'pain.001.001.05']: + 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' + requested_exec_date_2_17 = etree.SubElement( + payment_info_2_0, 'ReqdExctnDt') + requested_exec_date_2_17.text = my_requested_exec_date + debtor_2_19 = etree.SubElement(payment_info_2_0, 'Dbtr') + debtor_name = etree.SubElement(debtor_2_19, 'Nm') + debtor_name.text = my_company_name + debtor_account_2_20 = etree.SubElement(payment_info_2_0, 'DbtrAcct') + debtor_account_id = etree.SubElement(debtor_account_2_20, 'Id') debtor_account_iban = etree.SubElement(debtor_account_id, 'IBAN') - debtor_account_iban.text = my_company_iban - debtor_agent = etree.SubElement(payment_info, 'DbtrAgt') - debtor_agent_institution = etree.SubElement(debtor_agent, 'FinInstnId') - if my_company_bic: - debtor_agent_bic = etree.SubElement(debtor_agent_institution, bic_xml_tag) - debtor_agent_bic.text = my_company_bic - charge_bearer = etree.SubElement(payment_info, 'ChrgBr') - charge_bearer.text = sepa_export.charge_bearer + debtor_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) + debtor_agent_2_21 = etree.SubElement(payment_info_2_0, 'DbtrAgt') + debtor_agent_institution = etree.SubElement( + debtor_agent_2_21, 'FinInstnId') + debtor_agent_bic = etree.SubElement( + debtor_agent_institution, bic_xml_tag) + # TODO validate BIC with pattern + # [A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1} + # because OpenERP doesn't have a constraint on BIC + debtor_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 transactions_count = 0 total_amount = 0.0 @@ -226,88 +325,100 @@ class banking_export_sepa_wizard(orm.TransientModel): for line in payment_order.line_ids: transactions_count += 1 # C. Credit Transfer Transaction Info - credit_transfer_transaction_info = etree.SubElement(payment_info, 'CdtTrfTxInf') - payment_identification = etree.SubElement(credit_transfer_transaction_info, 'PmtId') - instruction_identification = etree.SubElement(payment_identification, 'InstrId') - instruction_identification.text = self._limit_size(cr, uid, line.communication, 35, context=context) #otherwise, we can reach the invoice fields via ml_inv_ref - end2end_identification = etree.SubElement(payment_identification, 'EndToEndId') - end2end_identification.text = self._limit_size(cr, uid, line.communication, 35, context=context) - amount = etree.SubElement(credit_transfer_transaction_info, 'Amt') - instructed_amount = etree.SubElement(amount, 'InstdAmt', Ccy=line.currency.name) - instructed_amount.text = '%.2f' % line.amount_currency + credit_transfer_transaction_info_2_27 = etree.SubElement( + payment_info_2_0, 'CdtTrfTxInf') + payment_identification_2_28 = etree.SubElement( + credit_transfer_transaction_info_2_27, 'PmtId') + end2end_identification_2_30 = etree.SubElement( + payment_identification_2_28, 'EndToEndId') + end2end_identification_2_30.text = self._prepare_field( + cr, uid, 'End to End Identification', 'line.name', 35, + line=line, context=context) + currency_name = self._prepare_field( + cr, uid, 'Currency Code', 'line.currency.name', 3, + line=line, context=context) + amount_2_42 = etree.SubElement( + credit_transfer_transaction_info_2_27, 'Amt') + instructed_amount_2_43 = etree.SubElement( + amount_2_42, 'InstdAmt', Ccy=currency_name) + instructed_amount_2_43.text = '%.2f' % line.amount_currency amount_control_sum += line.amount_currency - creditor_agent = etree.SubElement(credit_transfer_transaction_info, 'CdtrAgt') - creditor_agent_institution = etree.SubElement(creditor_agent, 'FinInstnId') + creditor_agent_2_77 = etree.SubElement( + credit_transfer_transaction_info_2_27, 'CdtrAgt') + creditor_agent_institution = etree.SubElement( + creditor_agent_2_77, 'FinInstnId') if not line.bank_id: - raise orm.except_orm(_('Error :'), _("Missing Bank Account on invoice '%s' (payment order line reference '%s').") %(line.ml_inv_ref.number, line.name)) - if line.bank_id.bank.bic: - creditor_agent_bic = etree.SubElement(creditor_agent_institution, bic_xml_tag) - creditor_agent_bic.text = line.bank_id.bank.bic - creditor = etree.SubElement(credit_transfer_transaction_info, 'Cdtr') - creditor_name = etree.SubElement(creditor, 'Nm') - creditor_name.text = self._limit_size(cr, uid, line.partner_id.name, name_maxsize, context=context) -# I don't think they want it -# If they want it, we need to implement full spec p26 appendix -# creditor_address = etree.SubElement(creditor, 'PstlAdr') -# creditor_street = etree.SubElement(creditor_address, 'AdrLine') -# creditor_street.text = line.partner_id.address[0].street -# creditor_city = etree.SubElement(creditor_address, 'AdrLine') -# creditor_city.text = line.partner_id.address[0].city -# creditor_country = etree.SubElement(creditor_address, 'Ctry') -# creditor_country.text = line.partner_id.address[0].country_id.code - creditor_account = etree.SubElement(credit_transfer_transaction_info, 'CdtrAcct') - creditor_account_id = etree.SubElement(creditor_account, 'Id') - creditor_account_iban = etree.SubElement(creditor_account_id, 'IBAN') - creditor_account_iban.text = self._validate_iban(cr, uid, line.bank_id.acc_number, context=context) - remittance_info = etree.SubElement(credit_transfer_transaction_info, '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 = etree.SubElement(remittance_info, 'Ustrd') - remittance_info_unstructured.text = self._limit_size(cr, uid, line.communication, 140, context=context) + raise orm.except_orm( + _('Error :'), + _("Missing Bank Account on invoice '%s' (payment order line reference '%s').") + % (line.ml_inv_ref.number, line.name)) + creditor_agent_bic = etree.SubElement( + creditor_agent_institution, bic_xml_tag) + creditor_agent_bic.text = self._prepare_field( + cr, uid, 'Customer BIC', 'line.bank_id.bank.bic', + line=line, context=context) + creditor_2_79 = etree.SubElement( + credit_transfer_transaction_info_2_27, 'Cdtr') + creditor_name = etree.SubElement(creditor_2_79, 'Nm') + creditor_name.text = self._prepare_field( + cr, uid, 'Customer Name', 'line.partner_id.name', + name_maxsize, line=line, context=context) + creditor_account_2_80 = etree.SubElement( + credit_transfer_transaction_info_2_27, 'CdtrAcct') + creditor_account_id = etree.SubElement( + creditor_account_2_80, 'Id') + creditor_account_iban = etree.SubElement( + creditor_account_id, 'IBAN') + creditor_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_91 = etree.SubElement( + credit_transfer_transaction_info_2_27, '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_99 = etree.SubElement( + remittance_info_2_91, 'Ustrd') + remittance_info_unstructured_2_99.text = self._prepare_field( + cr, uid, 'Remittance Information', 'line.communication', + 140, line=line, context=context) - if pain_flavor in ['pain.001.001.03', 'pain.001.001.04']: - nb_of_transactions_grphdr.text = nb_of_transactions_pmtinf.text = str(transactions_count) - control_sum_grphdr.text = control_sum_pmtinf.text = '%.2f' % amount_control_sum + if pain_flavor in [ + 'pain.001.001.03', 'pain.001.001.04', 'pain.001.001.05']: + 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 else: - nb_of_transactions_grphdr.text = str(transactions_count) - control_sum_grphdr.text = '%.2f' % amount_control_sum + nb_of_transactions_1_6.text = str(transactions_count) + control_sum_1_7.text = '%.2f' % amount_control_sum - - xml_string = etree.tostring(root, pretty_print=True, encoding='UTF-8', xml_declaration=True) - _logger.debug("Generated SEPA XML file below") + xml_string = etree.tostring( + root, pretty_print=True, encoding='UTF-8', xml_declaration=True) + _logger.debug( + "Generated SEPA Credit Transfer XML file in format %s below" + % pain_flavor) _logger.debug(xml_string) - official_pain_schema = etree.XMLSchema(etree.parse(tools.file_open('account_banking_sepa_credit_transfer/data/%s.xsd' % pain_flavor))) - - try: - 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)) + self._validate_xml(cr, uid, xml_string, pain_flavor) # CREATE the banking.export.sepa record - file_id = self.pool.get('banking.export.sepa').create(cr, uid, - { - 'msg_identification': my_msg_identification, - 'batch_booking': sepa_export.batch_booking, - 'charge_bearer': sepa_export.charge_bearer, - 'prefered_exec_date': sepa_export.prefered_exec_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) + file_id = self.pool.get('banking.export.sepa').create( + cr, uid, self._prepare_export_sepa( + cr, uid, sepa_export, total_amount, transactions_count, + xml_string, context=context), + context=context) - self.write(cr, uid, ids, { - 'file_id': file_id, - 'state': 'finish', + self.write( + cr, uid, ids, { + 'file_id': file_id, + 'state': 'finish', }, context=context) action = { - 'name': 'SEPA XML', + 'name': 'SEPA Credit Transfer XML', 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form,tree', @@ -317,26 +428,27 @@ class banking_export_sepa_wizard(orm.TransientModel): } return action - def cancel_sepa(self, cr, uid, ids, context=None): ''' Cancel the SEPA PAIN: just drop the file ''' sepa_export = self.browse(cr, uid, ids[0], context=context) - self.pool.get('banking.export.sepa').unlink(cr, uid, sepa_export.file_id.id, context=context) + self.pool.get('banking.export.sepa').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 PAIN: send the done signal to all payment orders in the file. - With the default workflow, they will transition to 'done', while with the - advanced workflow in account_banking_payment they will transition to 'sent' - waiting reconciliation. + Save the SEPA PAIN: send the done signal to all payment + orders in the file. With the default workflow, they will + transition to 'done', while with the advanced workflow in + account_banking_payment they will transition to 'sent' waiting + reconciliation. ''' sepa_export = self.browse(cr, uid, ids[0], context=context) - self.pool.get('banking.export.sepa').write(cr, uid, - sepa_export.file_id.id, {'state': 'sent'}, context=context) + self.pool.get('banking.export.sepa').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, 'done', cr) diff --git a/account_banking_sepa_credit_transfer/wizard/export_sepa_view.xml b/account_banking_sepa_credit_transfer/wizard/export_sepa_view.xml index 8dff532e3..793d0c8e0 100644 --- a/account_banking_sepa_credit_transfer/wizard/export_sepa_view.xml +++ b/account_banking_sepa_credit_transfer/wizard/export_sepa_view.xml @@ -18,17 +18,10 @@ - - - - +