Port SEPA modules to new API

Fix an important regression in account_banking_sepa_direct_debit: "Date of Last Debit" was not set any more
Proper write of date_done with account_banking_payment_export is installed without account_banking_payment_transfer
Add post-install script for date_sent on payment.order
This commit is contained in:
Alexis de Lattre
2015-06-06 00:20:41 +02:00
parent 7089fc150e
commit 3781f37bcb
19 changed files with 379 additions and 428 deletions

View File

@@ -32,8 +32,11 @@
<group name="main"> <group name="main">
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="partner_bank_id" <field name="partner_bank_id"
invisible="context.get('mandate_bank_partner_view')" /> invisible="context.get('mandate_bank_partner_view')"
<field name="partner_id" invisible="context.get('mandate_bank_partner_view')" readonly="True"/> domain="[('partner_id', '=', partner_id)]" />
<field name="partner_id"
invisible="context.get('mandate_bank_partner_view')"
readonly="True"/>
<field name="signature_date"/> <field name="signature_date"/>
<field name="scan"/> <field name="scan"/>
<field name="last_debit_date"/> <field name="last_debit_date"/>

View File

@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
############################################################################## ##############################################################################
# #
# PAIN Base module for OpenERP # PAIN Base module for Odoo
# Copyright (C) 2013 Akretion (http://www.akretion.com) # Copyright (C) 2013 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>
# #

View File

@@ -1,8 +1,8 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
############################################################################## ##############################################################################
# #
# PAIN base module for OpenERP # PAIN base module for Odoo
# Copyright (C) 2013 Akretion (http://www.akretion.com) # Copyright (C) 2013-2015 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify

View File

@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
############################################################################## ##############################################################################
# #
# PAIN Base module for OpenERP # PAIN Base module for Odoo
# Copyright (C) 2013 Akretion (http://www.akretion.com) # Copyright (C) 2013 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>
# #

View File

@@ -1,8 +1,8 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
############################################################################## ##############################################################################
# #
# PAIN Base module for OpenERP # PAIN Base module for Odoo
# Copyright (C) 2013 Akretion (http://www.akretion.com) # Copyright (C) 2013-2015 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@@ -20,8 +20,8 @@
# #
############################################################################## ##############################################################################
from openerp.osv import orm from openerp import models, api, _
from openerp.tools.translate import _ from openerp.exceptions import Warning
from openerp.tools.safe_eval import safe_eval from openerp.tools.safe_eval import safe_eval
from datetime import datetime from datetime import datetime
from lxml import etree from lxml import etree
@@ -38,21 +38,19 @@ except ImportError:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BankingExportPain(orm.AbstractModel): class BankingExportPain(models.AbstractModel):
_name = 'banking.export.pain' _name = 'banking.export.pain'
def _validate_iban(self, cr, uid, iban, context=None): def _validate_iban(self, iban):
"""if IBAN is valid, returns IBAN """if IBAN is valid, returns IBAN
if IBAN is NOT valid, raises an error message""" if IBAN is NOT valid, raises an error message"""
partner_bank_obj = self.pool.get('res.partner.bank') if self.env['res.partner.bank'].is_iban_valid(iban):
if partner_bank_obj.is_iban_valid(cr, uid, iban, context=context):
return iban.replace(' ', '') return iban.replace(' ', '')
else: else:
raise orm.except_orm( raise Warning(_("This IBAN is not valid : %s") % iban)
_('Error:'), _("This IBAN is not valid : %s") % iban)
def _prepare_field(self, cr, uid, field_name, field_value, eval_ctx, def _prepare_field(self, field_name, field_value, eval_ctx,
max_size=0, gen_args=None, context=None): max_size=0, gen_args=None):
"""This function is designed to be inherited !""" """This function is designed to be inherited !"""
if gen_args is None: if gen_args is None:
gen_args = {} gen_args = {}
@@ -74,31 +72,27 @@ class BankingExportPain(orm.AbstractModel):
except: except:
line = eval_ctx.get('line') line = eval_ctx.get('line')
if line: if line:
raise orm.except_orm( raise Warning(
_('Error:'),
_("Cannot compute the '%s' of the Payment Line with " _("Cannot compute the '%s' of the Payment Line with "
"reference '%s'.") "reference '%s'.")
% (field_name, line.name)) % (field_name, line.name))
else: else:
raise orm.except_orm( raise Warning(
_('Error:'),
_("Cannot compute the '%s'.") % field_name) _("Cannot compute the '%s'.") % field_name)
if not isinstance(value, (str, unicode)): if not isinstance(value, (str, unicode)):
raise orm.except_orm( raise Warning(
_('Field type error:'),
_("The type of the field '%s' is %s. It should be a string " _("The type of the field '%s' is %s. It should be a string "
"or unicode.") "or unicode.")
% (field_name, type(value))) % (field_name, type(value)))
if not value: if not value:
raise orm.except_orm( raise Warning(
_('Error:'),
_("The '%s' is empty or 0. It should have a non-null value.") _("The '%s' is empty or 0. It should have a non-null value.")
% field_name) % field_name)
if max_size and len(value) > max_size: if max_size and len(value) > max_size:
value = value[0:max_size] value = value[0:max_size]
return value return value
def _validate_xml(self, cr, uid, xml_string, gen_args, context=None): def _validate_xml(self, xml_string, gen_args):
xsd_etree_obj = etree.parse( xsd_etree_obj = etree.parse(
tools.file_open(gen_args['pain_xsd_file'])) tools.file_open(gen_args['pain_xsd_file']))
official_pain_schema = etree.XMLSchema(xsd_etree_obj) official_pain_schema = etree.XMLSchema(xsd_etree_obj)
@@ -111,19 +105,18 @@ class BankingExportPain(orm.AbstractModel):
"The XML file is invalid against the XML Schema Definition") "The XML file is invalid against the XML Schema Definition")
logger.warning(xml_string) logger.warning(xml_string)
logger.warning(e) logger.warning(e)
raise orm.except_orm( raise Warning(
_('Error:'),
_("The generated XML file is not valid against the official " _("The generated XML file is not valid against the official "
"XML Schema Definition. The generated XML file and the " "XML Schema Definition. The generated XML file and the "
"full error have been written in the server logs. Here " "full error have been written in the server logs. Here "
"is the error, which may give you an idea on the cause " "is the error, which may give you an idea on the cause "
"of the problem : %s") "of the problem : %s")
% str(e)) % unicode(e))
return True return True
@api.multi
def finalize_sepa_file_creation( def finalize_sepa_file_creation(
self, cr, uid, ids, xml_root, total_amount, transactions_count, self, xml_root, total_amount, transactions_count, gen_args):
gen_args, context=None):
xml_string = etree.tostring( xml_string = etree.tostring(
xml_root, pretty_print=True, encoding='UTF-8', xml_root, pretty_print=True, encoding='UTF-8',
xml_declaration=True) xml_declaration=True)
@@ -131,44 +124,41 @@ class BankingExportPain(orm.AbstractModel):
"Generated SEPA XML file in format %s below" "Generated SEPA XML file in format %s below"
% gen_args['pain_flavor']) % gen_args['pain_flavor'])
logger.debug(xml_string) logger.debug(xml_string)
self._validate_xml(cr, uid, xml_string, gen_args, context=context) self._validate_xml(xml_string, gen_args)
order_ref = [] order_ref = []
for order in gen_args['sepa_export'].payment_order_ids: for order in self.payment_order_ids:
if order.reference: if order.reference:
order_ref.append(order.reference.replace('/', '-')) order_ref.append(order.reference.replace('/', '-'))
filename = '%s%s.xml' % (gen_args['file_prefix'], '-'.join(order_ref)) filename = '%s%s.xml' % (gen_args['file_prefix'], '-'.join(order_ref))
self.write( self.write({
cr, uid, ids, { 'nb_transactions': transactions_count,
'nb_transactions': transactions_count, 'total_amount': total_amount,
'total_amount': total_amount, 'filename': filename,
'filename': filename, 'file': base64.encodestring(xml_string),
'file': base64.encodestring(xml_string), 'state': 'finish',
'state': 'finish', })
}, context=context)
action = { action = {
'name': 'SEPA File', 'name': _('SEPA File'),
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'view_type': 'form', 'view_type': 'form',
'view_mode': 'form,tree', 'view_mode': 'form,tree',
'res_model': self._name, 'res_model': self._name,
'res_id': ids[0], 'res_id': self.ids[0],
'target': 'new', 'target': 'new',
} }
return action return action
def generate_group_header_block( def generate_group_header_block(self, parent_node, gen_args):
self, cr, uid, parent_node, gen_args, context=None):
group_header_1_0 = etree.SubElement(parent_node, 'GrpHdr') group_header_1_0 = etree.SubElement(parent_node, 'GrpHdr')
message_identification_1_1 = etree.SubElement( message_identification_1_1 = etree.SubElement(
group_header_1_0, 'MsgId') group_header_1_0, 'MsgId')
message_identification_1_1.text = self._prepare_field( message_identification_1_1.text = self._prepare_field(
cr, uid, 'Message Identification', 'Message Identification',
'sepa_export.payment_order_ids[0].reference', 'self.payment_order_ids[0].reference',
{'sepa_export': gen_args['sepa_export']}, 35, {'self': self}, 35, gen_args=gen_args)
gen_args=gen_args, context=context)
creation_date_time_1_2 = etree.SubElement(group_header_1_0, 'CreDtTm') creation_date_time_1_2 = etree.SubElement(group_header_1_0, 'CreDtTm')
creation_date_time_1_2.text = datetime.strftime( creation_date_time_1_2.text = datetime.strftime(
datetime.today(), '%Y-%m-%dT%H:%M:%S') datetime.today(), '%Y-%m-%dT%H:%M:%S')
@@ -176,8 +166,7 @@ class BankingExportPain(orm.AbstractModel):
# batch_booking is in "Group header" with 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 # and in "Payment info" in pain.001.001.03/04
batch_booking = etree.SubElement(group_header_1_0, 'BtchBookg') batch_booking = etree.SubElement(group_header_1_0, 'BtchBookg')
batch_booking.text = \ batch_booking.text = unicode(self.batch_booking).lower()
str(gen_args['sepa_export'].batch_booking).lower()
nb_of_transactions_1_6 = etree.SubElement( nb_of_transactions_1_6 = etree.SubElement(
group_header_1_0, 'NbOfTxs') group_header_1_0, 'NbOfTxs')
control_sum_1_7 = etree.SubElement(group_header_1_0, 'CtrlSum') control_sum_1_7 = etree.SubElement(group_header_1_0, 'CtrlSum')
@@ -185,30 +174,26 @@ class BankingExportPain(orm.AbstractModel):
if gen_args.get('pain_flavor') == 'pain.001.001.02': if gen_args.get('pain_flavor') == 'pain.001.001.02':
grouping = etree.SubElement(group_header_1_0, 'Grpg') grouping = etree.SubElement(group_header_1_0, 'Grpg')
grouping.text = 'GRPD' grouping.text = 'GRPD'
self.generate_initiating_party_block( self.generate_initiating_party_block(group_header_1_0, gen_args)
cr, uid, group_header_1_0, gen_args,
context=context)
return group_header_1_0, nb_of_transactions_1_6, control_sum_1_7 return group_header_1_0, nb_of_transactions_1_6, control_sum_1_7
def generate_start_payment_info_block( def generate_start_payment_info_block(
self, cr, uid, parent_node, payment_info_ident, self, parent_node, payment_info_ident,
priority, local_instrument, sequence_type, requested_date, priority, local_instrument, sequence_type, requested_date,
eval_ctx, gen_args, context=None): eval_ctx, gen_args):
payment_info_2_0 = etree.SubElement(parent_node, 'PmtInf') payment_info_2_0 = etree.SubElement(parent_node, 'PmtInf')
payment_info_identification_2_1 = etree.SubElement( payment_info_identification_2_1 = etree.SubElement(
payment_info_2_0, 'PmtInfId') payment_info_2_0, 'PmtInfId')
payment_info_identification_2_1.text = self._prepare_field( payment_info_identification_2_1.text = self._prepare_field(
cr, uid, 'Payment Information Identification', 'Payment Information Identification',
payment_info_ident, eval_ctx, 35, payment_info_ident, eval_ctx, 35, gen_args=gen_args)
gen_args=gen_args, context=context)
payment_method_2_2 = etree.SubElement(payment_info_2_0, 'PmtMtd') payment_method_2_2 = etree.SubElement(payment_info_2_0, 'PmtMtd')
payment_method_2_2.text = gen_args['payment_method'] payment_method_2_2.text = gen_args['payment_method']
nb_of_transactions_2_4 = False nb_of_transactions_2_4 = False
control_sum_2_5 = False control_sum_2_5 = False
if gen_args.get('pain_flavor') != 'pain.001.001.02': if gen_args.get('pain_flavor') != 'pain.001.001.02':
batch_booking_2_3 = etree.SubElement(payment_info_2_0, 'BtchBookg') batch_booking_2_3 = etree.SubElement(payment_info_2_0, 'BtchBookg')
batch_booking_2_3.text = \ batch_booking_2_3.text = unicode(self.batch_booking).lower()
str(gen_args['sepa_export'].batch_booking).lower()
# The "SEPA Customer-to-bank # The "SEPA Customer-to-bank
# Implementation guidelines" for SCT and SDD says that control sum # Implementation guidelines" for SCT and SDD says that control sum
# and nb_of_transactions should be present # and nb_of_transactions should be present
@@ -246,26 +231,24 @@ class BankingExportPain(orm.AbstractModel):
requested_date_2_17.text = requested_date requested_date_2_17.text = requested_date
return payment_info_2_0, nb_of_transactions_2_4, control_sum_2_5 return payment_info_2_0, nb_of_transactions_2_4, control_sum_2_5
def _must_have_initiating_party(self, cr, uid, gen_args, context=None): def _must_have_initiating_party(self, gen_args):
'''This method is designed to be inherited in localization modules for '''This method is designed to be inherited in localization modules for
countries in which the initiating party is required''' countries in which the initiating party is required'''
return False return False
def generate_initiating_party_block( def generate_initiating_party_block(self, parent_node, gen_args):
self, cr, uid, parent_node, gen_args, context=None):
my_company_name = self._prepare_field( my_company_name = self._prepare_field(
cr, uid, 'Company Name', 'Company Name',
'sepa_export.payment_order_ids[0].mode.bank_id.partner_id.name', 'self.payment_order_ids[0].mode.bank_id.partner_id.name',
{'sepa_export': gen_args['sepa_export']}, {'self': self}, gen_args.get('name_maxsize'), gen_args=gen_args)
gen_args.get('name_maxsize'), gen_args=gen_args, context=context)
initiating_party_1_8 = etree.SubElement(parent_node, 'InitgPty') initiating_party_1_8 = etree.SubElement(parent_node, 'InitgPty')
initiating_party_name = etree.SubElement(initiating_party_1_8, 'Nm') initiating_party_name = etree.SubElement(initiating_party_1_8, 'Nm')
initiating_party_name.text = my_company_name initiating_party_name.text = my_company_name
initiating_party_identifier =\ initiating_party_identifier =\
gen_args['sepa_export'].payment_order_ids[0].company_id.\ self.payment_order_ids[0].company_id.\
initiating_party_identifier initiating_party_identifier
initiating_party_issuer =\ initiating_party_issuer =\
gen_args['sepa_export'].payment_order_ids[0].company_id.\ self.payment_order_ids[0].company_id.\
initiating_party_issuer initiating_party_issuer
if initiating_party_identifier and initiating_party_issuer: if initiating_party_identifier and initiating_party_issuer:
iniparty_id = etree.SubElement(initiating_party_1_8, 'Id') iniparty_id = etree.SubElement(initiating_party_1_8, 'Id')
@@ -276,36 +259,33 @@ class BankingExportPain(orm.AbstractModel):
iniparty_org_other_issuer = etree.SubElement( iniparty_org_other_issuer = etree.SubElement(
iniparty_org_other, 'Issr') iniparty_org_other, 'Issr')
iniparty_org_other_issuer.text = initiating_party_issuer iniparty_org_other_issuer.text = initiating_party_issuer
elif self._must_have_initiating_party(cr, uid, gen_args, elif self._must_have_initiating_party(gen_args):
context=context): raise Warning(
raise orm.except_orm(
_('Error:'),
_("Missing 'Initiating Party Issuer' and/or " _("Missing 'Initiating Party Issuer' and/or "
"'Initiating Party Identifier' for the company '%s'. " "'Initiating Party Identifier' for the company '%s'. "
"Both fields must have a value.") "Both fields must have a value.")
% gen_args['sepa_export'].payment_order_ids[0].company_id.name) % self.payment_order_ids[0].company_id.name)
return True return True
def generate_party_agent( def generate_party_agent(
self, cr, uid, parent_node, party_type, party_type_label, self, parent_node, party_type, party_type_label,
order, party_name, iban, bic, eval_ctx, gen_args, context=None): order, party_name, iban, bic, eval_ctx, gen_args):
"""Generate the piece of the XML file corresponding to BIC """Generate the piece of the XML file corresponding to BIC
This code is mutualized between TRF and DD""" This code is mutualized between TRF and DD"""
assert order in ('B', 'C'), "Order can be 'B' or 'C'" assert order in ('B', 'C'), "Order can be 'B' or 'C'"
try: try:
bic = self._prepare_field( bic = self._prepare_field(
cr, uid, '%s BIC' % party_type_label, bic, eval_ctx, '%s BIC' % party_type_label, bic, eval_ctx, gen_args=gen_args)
gen_args=gen_args, context=context)
party_agent = etree.SubElement(parent_node, '%sAgt' % party_type) party_agent = etree.SubElement(parent_node, '%sAgt' % party_type)
party_agent_institution = etree.SubElement( party_agent_institution = etree.SubElement(
party_agent, 'FinInstnId') party_agent, 'FinInstnId')
party_agent_bic = etree.SubElement( party_agent_bic = etree.SubElement(
party_agent_institution, gen_args.get('bic_xml_tag')) party_agent_institution, gen_args.get('bic_xml_tag'))
party_agent_bic.text = bic party_agent_bic.text = bic
except orm.except_orm: except Warning:
if order == 'C': if order == 'C':
if iban[0:2] != gen_args['initiating_party_country_code']: if iban[0:2] != gen_args['initiating_party_country_code']:
raise orm.except_orm( raise Warning(
_('Error:'), _('Error:'),
_("The bank account with IBAN '%s' of partner '%s' " _("The bank account with IBAN '%s' of partner '%s' "
"must have an associated BIC because it is a " "must have an associated BIC because it is a "
@@ -328,8 +308,8 @@ class BankingExportPain(orm.AbstractModel):
return True return True
def generate_party_block( def generate_party_block(
self, cr, uid, parent_node, party_type, order, name, iban, bic, self, parent_node, party_type, order, name, iban, bic,
eval_ctx, gen_args, context=None): eval_ctx, gen_args):
"""Generate the piece of the XML file corresponding to Name+IBAN+BIC """Generate the piece of the XML file corresponding to Name+IBAN+BIC
This code is mutualized between TRF and DD""" This code is mutualized between TRF and DD"""
assert order in ('B', 'C'), "Order can be 'B' or 'C'" assert order in ('B', 'C'), "Order can be 'B' or 'C'"
@@ -338,23 +318,19 @@ class BankingExportPain(orm.AbstractModel):
elif party_type == 'Dbtr': elif party_type == 'Dbtr':
party_type_label = 'Debtor' party_type_label = 'Debtor'
party_name = self._prepare_field( party_name = self._prepare_field(
cr, uid, '%s Name' % party_type_label, name, eval_ctx, '%s Name' % party_type_label, name, eval_ctx,
gen_args.get('name_maxsize'), gen_args.get('name_maxsize'), gen_args=gen_args)
gen_args=gen_args, context=context)
piban = self._prepare_field( piban = self._prepare_field(
cr, uid, '%s IBAN' % party_type_label, iban, eval_ctx, '%s IBAN' % party_type_label, iban, eval_ctx, gen_args=gen_args)
gen_args=gen_args, viban = self._validate_iban(piban)
context=context)
viban = self._validate_iban(cr, uid, piban, context=context)
# At C level, the order is : BIC, Name, IBAN # At C level, the order is : BIC, Name, IBAN
# At B level, the order is : Name, IBAN, BIC # At B level, the order is : Name, IBAN, BIC
if order == 'B': if order == 'B':
gen_args['initiating_party_country_code'] = viban[0:2] gen_args['initiating_party_country_code'] = viban[0:2]
elif order == 'C': elif order == 'C':
self.generate_party_agent( self.generate_party_agent(
cr, uid, parent_node, party_type, party_type_label, parent_node, party_type, party_type_label,
order, party_name, viban, bic, order, party_name, viban, bic, eval_ctx, gen_args)
eval_ctx, gen_args, context=context)
party = etree.SubElement(parent_node, party_type) party = etree.SubElement(parent_node, party_type)
party_nm = etree.SubElement(party, 'Nm') party_nm = etree.SubElement(party, 'Nm')
party_nm.text = party_name party_nm.text = party_name
@@ -366,13 +342,11 @@ class BankingExportPain(orm.AbstractModel):
party_account_iban.text = viban party_account_iban.text = viban
if order == 'B': if order == 'B':
self.generate_party_agent( self.generate_party_agent(
cr, uid, parent_node, party_type, party_type_label, parent_node, party_type, party_type_label,
order, party_name, viban, bic, order, party_name, viban, bic, eval_ctx, gen_args)
eval_ctx, gen_args, context=context)
return True return True
def generate_remittance_info_block( def generate_remittance_info_block(self, parent_node, line, gen_args):
self, cr, uid, parent_node, line, gen_args, context=None):
remittance_info_2_91 = etree.SubElement( remittance_info_2_91 = etree.SubElement(
parent_node, 'RmtInf') parent_node, 'RmtInf')
@@ -381,14 +355,12 @@ class BankingExportPain(orm.AbstractModel):
remittance_info_2_91, 'Ustrd') remittance_info_2_91, 'Ustrd')
remittance_info_unstructured_2_99.text = \ remittance_info_unstructured_2_99.text = \
self._prepare_field( self._prepare_field(
cr, uid, 'Remittance Unstructured Information', 'Remittance Unstructured Information',
'line.communication', {'line': line}, 140, 'line.communication', {'line': line}, 140,
gen_args=gen_args, gen_args=gen_args)
context=context)
else: else:
if not line.struct_communication_type: if not line.struct_communication_type:
raise orm.except_orm( raise Warning(
_('Error:'),
_("Missing 'Structured Communication Type' on payment " _("Missing 'Structured Communication Type' on payment "
"line with reference '%s'.") "line with reference '%s'.")
% line.name) % line.name)
@@ -422,22 +394,20 @@ class BankingExportPain(orm.AbstractModel):
line.struct_communication_type line.struct_communication_type
creditor_reference_2_126.text = \ creditor_reference_2_126.text = \
self._prepare_field( self._prepare_field(
cr, uid, 'Creditor Structured Reference', 'Creditor Structured Reference',
'line.communication', {'line': line}, 35, 'line.communication', {'line': line}, 35,
gen_args=gen_args, gen_args=gen_args)
context=context)
return True return True
def generate_creditor_scheme_identification( def generate_creditor_scheme_identification(
self, cr, uid, parent_node, identification, identification_label, self, parent_node, identification, identification_label,
eval_ctx, scheme_name_proprietary, gen_args, context=None): eval_ctx, scheme_name_proprietary, gen_args):
csi_id = etree.SubElement(parent_node, 'Id') csi_id = etree.SubElement(parent_node, 'Id')
csi_privateid = etree.SubElement(csi_id, 'PrvtId') csi_privateid = etree.SubElement(csi_id, 'PrvtId')
csi_other = etree.SubElement(csi_privateid, 'Othr') csi_other = etree.SubElement(csi_privateid, 'Othr')
csi_other_id = etree.SubElement(csi_other, 'Id') csi_other_id = etree.SubElement(csi_other, 'Id')
csi_other_id.text = self._prepare_field( csi_other_id.text = self._prepare_field(
cr, uid, identification_label, identification, eval_ctx, identification_label, identification, eval_ctx, gen_args=gen_args)
gen_args=gen_args, context=context)
csi_scheme_name = etree.SubElement(csi_other, 'SchmeNm') csi_scheme_name = etree.SubElement(csi_other, 'SchmeNm')
csi_scheme_name_proprietary = etree.SubElement( csi_scheme_name_proprietary = etree.SubElement(
csi_scheme_name, 'Prtry') csi_scheme_name, 'Prtry')

View File

@@ -1,8 +1,8 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
############################################################################## ##############################################################################
# #
# PAIN Base module for OpenERP # PAIN Base module for Odoo
# Copyright (C) 2013 Akretion (http://www.akretion.com) # Copyright (C) 2013-2015 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@@ -20,33 +20,23 @@
# #
############################################################################## ##############################################################################
from openerp.osv import orm, fields from openerp import models, fields
class PaymentLine(orm.Model): class PaymentLine(models.Model):
_inherit = 'payment.line' _inherit = 'payment.line'
def _get_struct_communication_types(self, cr, uid, context=None): def _get_struct_communication_types(self):
return [('ISO', 'ISO')] return [('ISO', 'ISO')]
_columns = { priority = fields.Selection([
'priority': fields.selection( ('NORM', 'Normal'),
[('NORM', 'Normal'), ('HIGH', 'High')],
('HIGH', 'High')], 'Priority', string='Priority', default='NORM',
help="This field will be used as the 'Instruction Priority' in " help="This field will be used as the 'Instruction Priority' in "
"the generated PAIN file."), "the generated PAIN file.")
# Update size from 64 to 140, because PAIN allows 140 caracters # Update size from 64 to 140, because PAIN allows 140 caracters
'communication': fields.char( communication = fields.Char(size=140)
'Communication', size=140, required=True, struct_communication_type = fields.Selection(
help="Used as the message between ordering customer and current " '_get_struct_communication_types',
"company. Depicts 'What do you want to say to the recipient " string='Structured Communication Type', default='ISO')
"about this order ?'"),
'struct_communication_type': fields.selection(
'_get_struct_communication_types',
'Structured Communication Type'),
}
_defaults = {
'priority': 'NORM',
'struct_communication_type': 'ISO',
}

View File

@@ -1,8 +1,8 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
############################################################################## ##############################################################################
# #
# PAIN Base module for OpenERP # PAIN Base module for Odoo
# Copyright (C) 2013 Akretion (http://www.akretion.com) # Copyright (C) 2013-2015 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@@ -20,20 +20,14 @@
# #
############################################################################## ##############################################################################
from openerp.osv import orm, fields from openerp import models, fields
class payment_mode(orm.Model): class PaymentMode(models.Model):
_inherit = 'payment.mode' _inherit = 'payment.mode'
_columns = { convert_to_ascii = fields.Boolean(
'convert_to_ascii': fields.boolean( string='Convert to ASCII', default=True,
'Convert to ASCII', help="If active, Odoo will convert each accented caracter to "
help="If active, Odoo will convert each accented caracter to " "the corresponding unaccented caracter, so that only ASCII "
"the corresponding unaccented caracter, so that only ASCII " "caracters are used in the generated PAIN file.")
"caracters are used in the generated PAIN file."),
}
_defaults = {
'convert_to_ascii': True,
}

View File

@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
############################################################################## ##############################################################################
# #
# PAIN Base module for OpenERP # PAIN Base module for Odoo
# Copyright (C) 2013-2015 Akretion (http://www.akretion.com) # Copyright (C) 2013-2015 Akretion (http://www.akretion.com)
# Copyright (C) 2013 Noviat (http://www.noviat.com) # Copyright (C) 2013 Noviat (http://www.noviat.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>

View File

@@ -44,6 +44,7 @@
'wizard/bank_payment_manual.xml', 'wizard/bank_payment_manual.xml',
'wizard/payment_order_create_view.xml', 'wizard/payment_order_create_view.xml',
'data/payment_mode_type.xml', 'data/payment_mode_type.xml',
'workflow/account_payment.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
], ],
'demo': ['demo/banking_demo.xml'], 'demo': ['demo/banking_demo.xml'],

View File

@@ -73,3 +73,11 @@ class PaymentOrder(models.Model):
workflow.trg_validate(self.env.uid, 'payment.order', workflow.trg_validate(self.env.uid, 'payment.order',
order_id, 'done', self.env.cr) order_id, 'done', self.env.cr)
return {} return {}
@api.multi
def action_done(self):
self.write({
'date_done': fields.Date.context_today(self),
'state': 'done',
})
return True

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Akretion (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
The licence is in the file __openerp__.py
-->
<openerp>
<data>
<record id="account_payment.act_done" model="workflow.activity">
<field name="action">action_done()</field>
</record>
</data>
</openerp>

View File

@@ -1 +1,2 @@
from . import model from . import model
from .post_install import set_date_sent

View File

@@ -31,6 +31,7 @@
'author': "Banking addons community,Odoo Community Association (OCA)", 'author': "Banking addons community,Odoo Community Association (OCA)",
'website': 'https://github.com/OCA/banking', 'website': 'https://github.com/OCA/banking',
'category': 'Banking addons', 'category': 'Banking addons',
'post_init_hook': 'set_date_sent',
'depends': [ 'depends': [
'account_banking_payment_export', 'account_banking_payment_export',
], ],

View File

@@ -98,9 +98,7 @@ class PaymentOrder(models.Model):
def action_done(self): def action_done(self):
for line in self.line_ids: for line in self.line_ids:
line.date_done = fields.Date.context_today(self) line.date_done = fields.Date.context_today(self)
self.date_done = fields.Date.context_today(self) return super(PaymentOrder, self).action_done()
# state is written in workflow definition
return True
@api.multi @api.multi
def _get_transfer_move_lines(self): def _get_transfer_move_lines(self):

View File

@@ -0,0 +1,25 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Account Banking Payment Transfer module for Odoo
# Copyright (C) 2015 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
def set_date_sent(cr, pool):
cr.execute('UPDATE payment_order set date_sent=date_done')

View File

@@ -68,8 +68,6 @@ write({'state':'rejected'})</field>
unfortunately. unfortunately.
--> -->
<record id="account_payment.act_done" model="workflow.activity"> <record id="account_payment.act_done" model="workflow.activity">
<field name="action">action_done()
write({'state':'done'})</field>
<field name="flow_stop" eval="False"/> <field name="flow_stop" eval="False"/>
</record> </record>

View File

@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
############################################################################## ##############################################################################
# #
# SEPA Credit Transfer module for OpenERP # SEPA Credit Transfer module for Odoo
# Copyright (C) 2010-2013 Akretion (http://www.akretion.com) # Copyright (C) 2010-2013 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>
# #

View File

@@ -21,75 +21,65 @@
############################################################################## ##############################################################################
from openerp.osv import orm, fields from openerp import models, fields, api, _
from openerp.tools.translate import _ from openerp.exceptions import Warning
from openerp import workflow from openerp import workflow
from lxml import etree from lxml import etree
class BankingExportSepaWizard(orm.TransientModel): class BankingExportSepaWizard(models.TransientModel):
_name = 'banking.export.sepa.wizard' _name = 'banking.export.sepa.wizard'
_inherit = ['banking.export.pain'] _inherit = ['banking.export.pain']
_description = 'Export SEPA Credit Transfer File' _description = 'Export SEPA Credit Transfer File'
_columns = { state = fields.Selection([
'state': fields.selection([('create', 'Create'), ('create', 'Create'),
('finish', 'Finish')], 'State', ('finish', 'Finish')],
readonly=True), string='State', readonly=True, default='create')
'batch_booking': fields.boolean( batch_booking = fields.Boolean(
'Batch Booking', string='Batch Booking',
help="If true, the bank statement will display only one debit " help="If true, the bank statement will display only one debit "
"line for all the wire transfers of the SEPA XML file ; if " "line for all the wire transfers of the SEPA XML file ; if "
"false, the bank statement will display one debit line per wire " "false, the bank statement will display one debit line per wire "
"transfer of the SEPA XML file."), "transfer of the SEPA XML file.")
'charge_bearer': fields.selection( charge_bearer = fields.Selection([
[('SLEV', 'Following Service Level'), ('SLEV', 'Following Service Level'),
('SHAR', 'Shared'), ('SHAR', 'Shared'),
('CRED', 'Borne by Creditor'), ('CRED', 'Borne by Creditor'),
('DEBT', 'Borne by Debtor')], 'Charge Bearer', required=True, ('DEBT', 'Borne by Debtor')], string='Charge Bearer',
help="Following service level : transaction charges are to be " default='SLEV', required=True,
"applied following the rules agreed in the service level " help="Following service level : transaction charges are to be "
"and/or scheme (SEPA Core messages must use this). Shared : " "applied following the rules agreed in the service level "
"transaction charges on the debtor side are to be borne by " "and/or scheme (SEPA Core messages must use this). Shared : "
"the debtor, transaction charges on the creditor side are to " "transaction charges on the debtor side are to be borne by "
"be borne by the creditor. Borne by creditor : all " "the debtor, transaction charges on the creditor side are to "
"transaction charges are to be borne by the creditor. Borne " "be borne by the creditor. Borne by creditor : all "
"by debtor : all transaction charges are to be borne by the " "transaction charges are to be borne by the creditor. Borne "
"debtor."), "by debtor : all transaction charges are to be borne by the "
'nb_transactions': fields.integer( "debtor.")
string='Number of Transactions', readonly=True), nb_transactions = fields.Integer(
'total_amount': fields.float( string='Number of Transactions', readonly=True)
string='Total Amount', readonly=True), total_amount = fields.Float(string='Total Amount', readonly=True)
'file': fields.binary( file = fields.Binary(string="File", readonly=True)
string="File", readonly=True), filename = fields.Char(string="Filename", readonly=True)
'filename': fields.char( payment_order_ids = fields.Many2many(
string="Filename", readonly=True), 'payment.order', 'wiz_sepa_payorders_rel', 'wizard_id',
'payment_order_ids': fields.many2many( 'payment_order_id', string='Payment Orders', readonly=True)
'payment.order', 'wiz_sepa_payorders_rel', 'wizard_id',
'payment_order_id', 'Payment Orders', readonly=True),
}
_defaults = { @api.model
'charge_bearer': 'SLEV', def create(self, vals):
'state': 'create', payment_order_ids = self._context.get('active_ids', [])
}
def create(self, cr, uid, vals, context=None):
payment_order_ids = context.get('active_ids', [])
vals.update({ vals.update({
'payment_order_ids': [[6, 0, payment_order_ids]], 'payment_order_ids': [[6, 0, payment_order_ids]],
}) })
return super(BankingExportSepaWizard, self).create( return super(BankingExportSepaWizard, self).create(vals)
cr, uid, vals, context=context)
def create_sepa(self, cr, uid, ids, context=None): @api.multi
def create_sepa(self):
"""Creates the SEPA Credit Transfer file. That's the important code!""" """Creates the SEPA Credit Transfer file. That's the important code!"""
if context is None: pain_flavor = self.payment_order_ids[0].mode.type.code
context = {}
sepa_export = self.browse(cr, uid, ids[0], context=context)
pain_flavor = sepa_export.payment_order_ids[0].mode.type.code
convert_to_ascii = \ convert_to_ascii = \
sepa_export.payment_order_ids[0].mode.convert_to_ascii self.payment_order_ids[0].mode.convert_to_ascii
if pain_flavor == 'pain.001.001.02': if pain_flavor == 'pain.001.001.02':
bic_xml_tag = 'BIC' bic_xml_tag = 'BIC'
name_maxsize = 70 name_maxsize = 70
@@ -115,8 +105,7 @@ class BankingExportSepaWizard(orm.TransientModel):
name_maxsize = 140 name_maxsize = 140
root_xml_tag = 'CstmrCdtTrfInitn' root_xml_tag = 'CstmrCdtTrfInitn'
else: else:
raise orm.except_orm( raise Warning(
_('Error:'),
_("Payment Type Code '%s' is not supported. The only " _("Payment Type Code '%s' is not supported. The only "
"Payment Type Codes supported for SEPA Credit Transfers " "Payment Type Codes supported for SEPA Credit Transfers "
"are 'pain.001.001.02', 'pain.001.001.03', " "are 'pain.001.001.02', 'pain.001.001.03', "
@@ -129,7 +118,6 @@ class BankingExportSepaWizard(orm.TransientModel):
'payment_method': 'TRF', 'payment_method': 'TRF',
'file_prefix': 'sct_', 'file_prefix': 'sct_',
'pain_flavor': pain_flavor, 'pain_flavor': pain_flavor,
'sepa_export': sepa_export,
'pain_xsd_file': 'pain_xsd_file':
'account_banking_sepa_credit_transfer/data/%s.xsd' 'account_banking_sepa_credit_transfer/data/%s.xsd'
% pain_flavor, % pain_flavor,
@@ -144,16 +132,15 @@ class BankingExportSepaWizard(orm.TransientModel):
['pain.001.001.03', 'pain.001.001.04', 'pain.001.001.05'] ['pain.001.001.03', 'pain.001.001.04', 'pain.001.001.05']
# A. Group header # A. Group header
group_header_1_0, nb_of_transactions_1_6, control_sum_1_7 = \ group_header_1_0, nb_of_transactions_1_6, control_sum_1_7 = \
self.generate_group_header_block( self.generate_group_header_block(pain_root, gen_args)
cr, uid, pain_root, gen_args, context=context)
transactions_count_1_6 = 0 transactions_count_1_6 = 0
total_amount = 0.0 total_amount = 0.0
amount_control_sum_1_7 = 0.0 amount_control_sum_1_7 = 0.0
lines_per_group = {} lines_per_group = {}
# key = (requested_date, priority) # key = (requested_date, priority)
# values = list of lines as object # values = list of lines as object
today = fields.date.context_today(self, cr, uid, context=context) today = fields.Date.context_today(self)
for payment_order in sepa_export.payment_order_ids: for payment_order in self.payment_order_ids:
total_amount = total_amount + payment_order.total total_amount = total_amount + payment_order.total
for line in payment_order.line_ids: for line in payment_order.line_ids:
priority = line.priority priority = line.priority
@@ -170,32 +157,30 @@ class BankingExportSepaWizard(orm.TransientModel):
lines_per_group[key] = [line] lines_per_group[key] = [line]
# Write requested_date on 'Payment date' of the pay line # Write requested_date on 'Payment date' of the pay line
if requested_date != line.date: if requested_date != line.date:
self.pool['payment.line'].write( line.date = requested_date
cr, uid, line.id,
{'date': requested_date}, context=context)
for (requested_date, priority), lines in lines_per_group.items(): for (requested_date, priority), lines in lines_per_group.items():
# B. Payment info # B. Payment info
payment_info_2_0, nb_of_transactions_2_4, control_sum_2_5 = \ payment_info_2_0, nb_of_transactions_2_4, control_sum_2_5 = \
self.generate_start_payment_info_block( self.generate_start_payment_info_block(
cr, uid, pain_root, pain_root,
"sepa_export.payment_order_ids[0].reference + '-' " "self.payment_order_ids[0].reference + '-' "
"+ requested_date.replace('-', '') + '-' + priority", "+ requested_date.replace('-', '') + '-' + priority",
priority, False, False, requested_date, { priority, False, False, requested_date, {
'sepa_export': sepa_export, 'self': self,
'priority': priority, 'priority': priority,
'requested_date': requested_date, 'requested_date': requested_date,
}, gen_args, context=context) }, gen_args)
self.generate_party_block( self.generate_party_block(
cr, uid, payment_info_2_0, 'Dbtr', 'B', payment_info_2_0, 'Dbtr', 'B',
'sepa_export.payment_order_ids[0].mode.bank_id.partner_id.' 'self.payment_order_ids[0].mode.bank_id.partner_id.'
'name', 'name',
'sepa_export.payment_order_ids[0].mode.bank_id.acc_number', 'self.payment_order_ids[0].mode.bank_id.acc_number',
'sepa_export.payment_order_ids[0].mode.bank_id.bank.bic or ' 'self.payment_order_ids[0].mode.bank_id.bank.bic or '
'sepa_export.payment_order_ids[0].mode.bank_id.bank_bic', 'self.payment_order_ids[0].mode.bank_id.bank_bic',
{'sepa_export': sepa_export}, {'self': self},
gen_args, context=context) gen_args)
charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr') charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr')
charge_bearer_2_24.text = sepa_export.charge_bearer charge_bearer_2_24.text = self.charge_bearer
transactions_count_2_4 = 0 transactions_count_2_4 = 0
amount_control_sum_2_5 = 0.0 amount_control_sum_2_5 = 0.0
for line in lines: for line in lines:
@@ -209,13 +194,11 @@ class BankingExportSepaWizard(orm.TransientModel):
end2end_identification_2_30 = etree.SubElement( end2end_identification_2_30 = etree.SubElement(
payment_identification_2_28, 'EndToEndId') payment_identification_2_28, 'EndToEndId')
end2end_identification_2_30.text = self._prepare_field( end2end_identification_2_30.text = self._prepare_field(
cr, uid, 'End to End Identification', 'line.name', 'End to End Identification', 'line.name',
{'line': line}, 35, gen_args=gen_args, {'line': line}, 35, gen_args=gen_args)
context=context)
currency_name = self._prepare_field( currency_name = self._prepare_field(
cr, uid, 'Currency Code', 'line.currency.name', 'Currency Code', 'line.currency.name',
{'line': line}, 3, gen_args=gen_args, {'line': line}, 3, gen_args=gen_args)
context=context)
amount_2_42 = etree.SubElement( amount_2_42 = etree.SubElement(
credit_transfer_transaction_info_2_27, 'Amt') credit_transfer_transaction_info_2_27, 'Amt')
instructed_amount_2_43 = etree.SubElement( instructed_amount_2_43 = etree.SubElement(
@@ -224,48 +207,44 @@ class BankingExportSepaWizard(orm.TransientModel):
amount_control_sum_1_7 += line.amount_currency amount_control_sum_1_7 += line.amount_currency
amount_control_sum_2_5 += line.amount_currency amount_control_sum_2_5 += line.amount_currency
if not line.bank_id: if not line.bank_id:
raise orm.except_orm( raise Warning(
_('Error:'),
_("Missing Bank Account on invoice '%s' (payment " _("Missing Bank Account on invoice '%s' (payment "
"order line reference '%s').") "order line reference '%s').")
% (line.ml_inv_ref.number, line.name)) % (line.ml_inv_ref.number, line.name))
self.generate_party_block( self.generate_party_block(
cr, uid, credit_transfer_transaction_info_2_27, 'Cdtr', credit_transfer_transaction_info_2_27, 'Cdtr',
'C', 'line.partner_id.name', 'line.bank_id.acc_number', 'C', 'line.partner_id.name', 'line.bank_id.acc_number',
'line.bank_id.bank.bic or ' 'line.bank_id.bank.bic or '
'line.bank_id.bank_bic', {'line': line}, gen_args, 'line.bank_id.bank_bic', {'line': line}, gen_args)
context=context)
self.generate_remittance_info_block( self.generate_remittance_info_block(
cr, uid, credit_transfer_transaction_info_2_27, credit_transfer_transaction_info_2_27, line, gen_args)
line, gen_args, context=context)
if pain_flavor in pain_03_to_05: if pain_flavor in pain_03_to_05:
nb_of_transactions_2_4.text = str(transactions_count_2_4) nb_of_transactions_2_4.text = unicode(transactions_count_2_4)
control_sum_2_5.text = '%.2f' % amount_control_sum_2_5 control_sum_2_5.text = '%.2f' % amount_control_sum_2_5
if pain_flavor in pain_03_to_05: if pain_flavor in pain_03_to_05:
nb_of_transactions_1_6.text = str(transactions_count_1_6) nb_of_transactions_1_6.text = unicode(transactions_count_1_6)
control_sum_1_7.text = '%.2f' % amount_control_sum_1_7 control_sum_1_7.text = '%.2f' % amount_control_sum_1_7
else: else:
nb_of_transactions_1_6.text = str(transactions_count_1_6) nb_of_transactions_1_6.text = unicode(transactions_count_1_6)
control_sum_1_7.text = '%.2f' % amount_control_sum_1_7 control_sum_1_7.text = '%.2f' % amount_control_sum_1_7
return self.finalize_sepa_file_creation( return self.finalize_sepa_file_creation(
cr, uid, ids, xml_root, total_amount, transactions_count_1_6, xml_root, total_amount, transactions_count_1_6, gen_args)
gen_args, context=context)
def save_sepa(self, cr, uid, ids, context=None): @api.multi
def save_sepa(self):
"""Save the SEPA file: send the done signal to all payment """Save the SEPA file: send the done signal to all payment
orders in the file. With the default workflow, they will orders in the file. With the default workflow, they will
transition to 'done', while with the advanced workflow in transition to 'done', while with the advanced workflow in
account_banking_payment they will transition to 'sent' waiting account_banking_payment they will transition to 'sent' waiting
reconciliation. reconciliation.
""" """
sepa_export = self.browse(cr, uid, ids[0], context=context) for order in self.payment_order_ids:
for order in sepa_export.payment_order_ids: workflow.trg_validate(
workflow.trg_validate(uid, 'payment.order', order.id, 'done', cr) self._uid, 'payment.order', order.id, 'done', self._cr)
self.pool['ir.attachment'].create( self.env['ir.attachment'].create({
cr, uid, { 'res_model': 'payment.order',
'res_model': 'payment.order', 'res_id': order.id,
'res_id': order.id, 'name': self.filename,
'name': sepa_export.filename, 'datas': self.file,
'datas': sepa_export.file, })
}, context=context)
return True return True

View File

@@ -21,99 +21,85 @@
############################################################################## ##############################################################################
from openerp.osv import orm, fields from openerp import models, fields, api, _
from openerp.tools.translate import _ from openerp.exceptions import Warning
from openerp import workflow from openerp import workflow
from lxml import etree from lxml import etree
class BankingExportSddWizard(orm.TransientModel): class BankingExportSddWizard(models.TransientModel):
_name = 'banking.export.sdd.wizard' _name = 'banking.export.sdd.wizard'
_inherit = ['banking.export.pain'] _inherit = ['banking.export.pain']
_description = 'Export SEPA Direct Debit File' _description = 'Export SEPA Direct Debit File'
_columns = {
'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 credit "
"line for all the direct debits of the SEPA file ; if false, "
"the bank statement will display one credit line per direct "
"debit of the SEPA file."),
'charge_bearer': fields.selection(
[('SLEV', 'Following Service Level'),
('SHAR', 'Shared'),
('CRED', 'Borne by Creditor'),
('DEBT', 'Borne by Debtor')], 'Charge Bearer', required=True,
help="Following service level : transaction charges are to be "
"applied following the rules agreed in the service level "
"and/or scheme (SEPA Core messages must use this). Shared : "
"transaction charges on the creditor side are to be borne "
"by the creditor, transaction charges on the debtor side are "
"to be borne by the debtor. 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."),
'nb_transactions': fields.integer(
string='Number of Transactions', readonly=True),
'total_amount': fields.float(
string='Total Amount', readonly=True),
'file': fields.binary(
string="File", readonly=True),
'filename': fields.char(
string="Filename", readonly=True),
'payment_order_ids': fields.many2many(
'payment.order', 'wiz_sdd_payorders_rel', 'wizard_id',
'payment_order_id', 'Payment Orders', readonly=True),
}
_defaults = { state = fields.Selection([
'charge_bearer': 'SLEV', ('create', 'Create'),
'state': 'create', ('finish', 'Finish'),
} ], string='State', readonly=True, default='create')
batch_booking = fields.Boolean(
string='Batch Booking',
help="If true, the bank statement will display only one credit "
"line for all the direct debits of the SEPA file ; if false, "
"the bank statement will display one credit line per direct "
"debit of the SEPA file.")
charge_bearer = fields.Selection([
('SLEV', 'Following Service Level'),
('SHAR', 'Shared'),
('CRED', 'Borne by Creditor'),
('DEBT', 'Borne by Debtor'),
], string='Charge Bearer', required=True, default='SLEV',
help="Following service level : transaction charges are to be "
"applied following the rules agreed in the service level "
"and/or scheme (SEPA Core messages must use this). Shared : "
"transaction charges on the creditor side are to be borne "
"by the creditor, transaction charges on the debtor side are "
"to be borne by the debtor. 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.")
nb_transactions = fields.Integer(
string='Number of Transactions', readonly=True)
total_amount = fields.Float(string='Total Amount', readonly=True)
file = fields.Binary(string="File", readonly=True)
filename = fields.Char(string="Filename", readonly=True)
payment_order_ids = fields.Many2many(
'payment.order', 'wiz_sdd_payorders_rel', 'wizard_id',
'payment_order_id', string='Payment Orders', readonly=True)
def create(self, cr, uid, vals, context=None): @api.model
payment_order_ids = context.get('active_ids', []) def create(self, vals):
payment_order_ids = self._context.get('active_ids', [])
vals.update({ vals.update({
'payment_order_ids': [[6, 0, payment_order_ids]], 'payment_order_ids': [[6, 0, payment_order_ids]],
}) })
return super(BankingExportSddWizard, self).create( return super(BankingExportSddWizard, self).create(vals)
cr, uid, vals, context=context)
def _get_previous_bank(self, cr, uid, payline, context=None): def _get_previous_bank(self, payline):
payline_obj = self.pool['payment.line']
previous_bank = False previous_bank = False
payline_ids = payline_obj.search( older_lines = self.env['payment.line'].search([
cr, uid, [ ('mandate_id', '=', payline.mandate_id.id),
('mandate_id', '=', payline.mandate_id.id), ('bank_id', '!=', payline.bank_id.id)])
('bank_id', '!=', payline.bank_id.id), if older_lines:
],
context=context)
if payline_ids:
older_lines = payline_obj.browse(
cr, uid, payline_ids, context=context)
previous_date = False previous_date = False
previous_payline_id = False previous_payline = False
for older_line in older_lines: for older_line in older_lines:
older_line_date_sent = older_line.order_id.date_sent if hasattr(older_line.order_id, 'date_sent'):
if (older_line_date_sent and older_line_date = older_line.order_id.date_sent
older_line_date_sent > previous_date): else:
previous_date = older_line_date_sent older_line_date = older_line.order_id.date_done
previous_payline_id = older_line.id if (older_line_date and
if previous_payline_id: older_line_date > previous_date):
previous_payline = payline_obj.browse( previous_date = older_line_date
cr, uid, previous_payline_id, context=context) previous_payline = older_line
if previous_payline:
previous_bank = previous_payline.bank_id previous_bank = previous_payline.bank_id
return previous_bank return previous_bank
def create_sepa(self, cr, uid, ids, context=None): @api.multi
def create_sepa(self):
"""Creates the SEPA Direct Debit file. That's the important code !""" """Creates the SEPA Direct Debit file. That's the important code !"""
sepa_export = self.browse(cr, uid, ids[0], context=context) pain_flavor = self.payment_order_ids[0].mode.type.code
pain_flavor = sepa_export.payment_order_ids[0].mode.type.code
convert_to_ascii = \ convert_to_ascii = \
sepa_export.payment_order_ids[0].mode.convert_to_ascii self.payment_order_ids[0].mode.convert_to_ascii
if pain_flavor == 'pain.008.001.02': if pain_flavor == 'pain.008.001.02':
bic_xml_tag = 'BIC' bic_xml_tag = 'BIC'
name_maxsize = 70 name_maxsize = 70
@@ -127,8 +113,7 @@ class BankingExportSddWizard(orm.TransientModel):
name_maxsize = 140 name_maxsize = 140
root_xml_tag = 'CstmrDrctDbtInitn' root_xml_tag = 'CstmrDrctDbtInitn'
else: else:
raise orm.except_orm( raise Warning(
_('Error:'),
_("Payment Type Code '%s' is not supported. The only " _("Payment Type Code '%s' is not supported. The only "
"Payment Type Code supported for SEPA Direct Debit are " "Payment Type Code supported for SEPA Direct Debit are "
"'pain.008.001.02', 'pain.008.001.03' and " "'pain.008.001.02', 'pain.008.001.03' and "
@@ -140,7 +125,6 @@ class BankingExportSddWizard(orm.TransientModel):
'payment_method': 'DD', 'payment_method': 'DD',
'file_prefix': 'sdd_', 'file_prefix': 'sdd_',
'pain_flavor': pain_flavor, 'pain_flavor': pain_flavor,
'sepa_export': sepa_export,
'pain_xsd_file': 'pain_xsd_file':
'account_banking_sepa_direct_debit/data/%s.xsd' % pain_flavor, 'account_banking_sepa_direct_debit/data/%s.xsd' % pain_flavor,
} }
@@ -152,8 +136,7 @@ class BankingExportSddWizard(orm.TransientModel):
pain_root = etree.SubElement(xml_root, root_xml_tag) pain_root = etree.SubElement(xml_root, root_xml_tag)
# A. Group header # A. Group header
group_header_1_0, nb_of_transactions_1_6, control_sum_1_7 = \ group_header_1_0, nb_of_transactions_1_6, control_sum_1_7 = \
self.generate_group_header_block( self.generate_group_header_block(pain_root, gen_args)
cr, uid, pain_root, gen_args, context=context)
transactions_count_1_6 = 0 transactions_count_1_6 = 0
total_amount = 0.0 total_amount = 0.0
amount_control_sum_1_7 = 0.0 amount_control_sum_1_7 = 0.0
@@ -161,8 +144,8 @@ class BankingExportSddWizard(orm.TransientModel):
# key = (requested_date, priority, sequence type) # key = (requested_date, priority, sequence type)
# value = list of lines as objects # value = list of lines as objects
# Iterate on payment orders # Iterate on payment orders
today = fields.date.context_today(self, cr, uid, context=context) today = fields.Date.context_today(self)
for payment_order in sepa_export.payment_order_ids: for payment_order in self.payment_order_ids:
total_amount = total_amount + payment_order.total total_amount = total_amount + payment_order.total
# Iterate each payment lines # Iterate each payment lines
for line in payment_order.line_ids: for line in payment_order.line_ids:
@@ -175,16 +158,14 @@ class BankingExportSddWizard(orm.TransientModel):
else: else:
requested_date = today requested_date = today
if not line.mandate_id: if not line.mandate_id:
raise orm.except_orm( raise Warning(
_('Error:'),
_("Missing SEPA Direct Debit mandate on the payment " _("Missing SEPA Direct Debit mandate on the payment "
"line with partner '%s' and Invoice ref '%s'.") "line with partner '%s' and Invoice ref '%s'.")
% (line.partner_id.name, % (line.partner_id.name,
line.ml_inv_ref.number)) line.ml_inv_ref.number))
scheme = line.mandate_id.scheme scheme = line.mandate_id.scheme
if line.mandate_id.state != 'valid': if line.mandate_id.state != 'valid':
raise orm.except_orm( raise Warning(
_('Error:'),
_("The SEPA Direct Debit mandate with reference '%s' " _("The SEPA Direct Debit mandate with reference '%s' "
"for partner '%s' has expired.") "for partner '%s' has expired.")
% (line.mandate_id.unique_mandate_reference, % (line.mandate_id.unique_mandate_reference,
@@ -192,8 +173,7 @@ class BankingExportSddWizard(orm.TransientModel):
if line.mandate_id.type == 'oneoff': if line.mandate_id.type == 'oneoff':
seq_type = 'OOFF' seq_type = 'OOFF'
if line.mandate_id.last_debit_date: if line.mandate_id.last_debit_date:
raise orm.except_orm( raise Warning(
_('Error:'),
_("The mandate with reference '%s' for partner " _("The mandate with reference '%s' for partner "
"'%s' has type set to 'One-Off' and it has a " "'%s' has type set to 'One-Off' and it has a "
"last debit date set to '%s', so we can't use " "last debit date set to '%s', so we can't use "
@@ -218,45 +198,41 @@ class BankingExportSddWizard(orm.TransientModel):
lines_per_group[key] = [line] lines_per_group[key] = [line]
# Write requested_exec_date on 'Payment date' of the pay line # Write requested_exec_date on 'Payment date' of the pay line
if requested_date != line.date: if requested_date != line.date:
self.pool['payment.line'].write( line.date = requested_date
cr, uid, line.id,
{'date': requested_date}, context=context)
for (requested_date, priority, sequence_type, scheme), lines in \ for (requested_date, priority, sequence_type, scheme), lines in \
lines_per_group.items(): lines_per_group.items():
# B. Payment info # B. Payment info
payment_info_2_0, nb_of_transactions_2_4, control_sum_2_5 = \ payment_info_2_0, nb_of_transactions_2_4, control_sum_2_5 = \
self.generate_start_payment_info_block( self.generate_start_payment_info_block(
cr, uid, pain_root, pain_root,
"sepa_export.payment_order_ids[0].reference + '-' + " "self.payment_order_ids[0].reference + '-' + "
"sequence_type + '-' + requested_date.replace('-', '') " "sequence_type + '-' + requested_date.replace('-', '') "
"+ '-' + priority", "+ '-' + priority",
priority, scheme, sequence_type, requested_date, { priority, scheme, sequence_type, requested_date, {
'sepa_export': sepa_export, 'self': self,
'sequence_type': sequence_type, 'sequence_type': sequence_type,
'priority': priority, 'priority': priority,
'requested_date': requested_date, 'requested_date': requested_date,
}, gen_args, context=context) }, gen_args)
self.generate_party_block( self.generate_party_block(
cr, uid, payment_info_2_0, 'Cdtr', 'B', payment_info_2_0, 'Cdtr', 'B',
'sepa_export.payment_order_ids[0].mode.bank_id.partner_id.' 'self.payment_order_ids[0].mode.bank_id.partner_id.'
'name', 'name',
'sepa_export.payment_order_ids[0].mode.bank_id.acc_number', 'self.payment_order_ids[0].mode.bank_id.acc_number',
'sepa_export.payment_order_ids[0].mode.bank_id.bank.bic or ' 'self.payment_order_ids[0].mode.bank_id.bank.bic or '
'sepa_export.payment_order_ids[0].mode.bank_id.bank_bic', 'self.payment_order_ids[0].mode.bank_id.bank_bic',
{'sepa_export': sepa_export}, {'self': self}, gen_args)
gen_args, context=context)
charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr') charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr')
charge_bearer_2_24.text = sepa_export.charge_bearer charge_bearer_2_24.text = self.charge_bearer
creditor_scheme_identification_2_27 = etree.SubElement( creditor_scheme_identification_2_27 = etree.SubElement(
payment_info_2_0, 'CdtrSchmeId') payment_info_2_0, 'CdtrSchmeId')
self.generate_creditor_scheme_identification( self.generate_creditor_scheme_identification(
cr, uid, creditor_scheme_identification_2_27, creditor_scheme_identification_2_27,
'sepa_export.payment_order_ids[0].company_id.' 'self.payment_order_ids[0].company_id.'
'sepa_creditor_identifier', 'sepa_creditor_identifier',
'SEPA Creditor Identifier', {'sepa_export': sepa_export}, 'SEPA Creditor Identifier', {'self': self}, 'SEPA', gen_args)
'SEPA', gen_args, context=context)
transactions_count_2_4 = 0 transactions_count_2_4 = 0
amount_control_sum_2_5 = 0.0 amount_control_sum_2_5 = 0.0
for line in lines: for line in lines:
@@ -269,13 +245,11 @@ class BankingExportSddWizard(orm.TransientModel):
end2end_identification_2_31 = etree.SubElement( end2end_identification_2_31 = etree.SubElement(
payment_identification_2_29, 'EndToEndId') payment_identification_2_29, 'EndToEndId')
end2end_identification_2_31.text = self._prepare_field( end2end_identification_2_31.text = self._prepare_field(
cr, uid, 'End to End Identification', 'line.name', 'End to End Identification', 'line.name',
{'line': line}, 35, {'line': line}, 35, gen_args=gen_args)
gen_args=gen_args, context=context)
currency_name = self._prepare_field( currency_name = self._prepare_field(
cr, uid, 'Currency Code', 'line.currency.name', 'Currency Code', 'line.currency.name',
{'line': line}, 3, gen_args=gen_args, {'line': line}, 3, gen_args=gen_args)
context=context)
instructed_amount_2_44 = etree.SubElement( instructed_amount_2_44 = etree.SubElement(
dd_transaction_info_2_28, 'InstdAmt', Ccy=currency_name) dd_transaction_info_2_28, 'InstdAmt', Ccy=currency_name)
instructed_amount_2_44.text = '%.2f' % line.amount_currency instructed_amount_2_44.text = '%.2f' % line.amount_currency
@@ -288,22 +262,19 @@ class BankingExportSddWizard(orm.TransientModel):
mandate_identification_2_48 = etree.SubElement( mandate_identification_2_48 = etree.SubElement(
mandate_related_info_2_47, 'MndtId') mandate_related_info_2_47, 'MndtId')
mandate_identification_2_48.text = self._prepare_field( mandate_identification_2_48.text = self._prepare_field(
cr, uid, 'Unique Mandate Reference', 'Unique Mandate Reference',
'line.mandate_id.unique_mandate_reference', 'line.mandate_id.unique_mandate_reference',
{'line': line}, 35, {'line': line}, 35, gen_args=gen_args)
gen_args=gen_args, context=context)
mandate_signature_date_2_49 = etree.SubElement( mandate_signature_date_2_49 = etree.SubElement(
mandate_related_info_2_47, 'DtOfSgntr') mandate_related_info_2_47, 'DtOfSgntr')
mandate_signature_date_2_49.text = self._prepare_field( mandate_signature_date_2_49.text = self._prepare_field(
cr, uid, 'Mandate Signature Date', 'Mandate Signature Date',
'line.mandate_id.signature_date', 'line.mandate_id.signature_date',
{'line': line}, 10, {'line': line}, 10, gen_args=gen_args)
gen_args=gen_args, context=context)
if sequence_type == 'FRST' and ( if sequence_type == 'FRST' and (
line.mandate_id.last_debit_date or line.mandate_id.last_debit_date or
not line.mandate_id.sepa_migrated): not line.mandate_id.sepa_migrated):
previous_bank = self._get_previous_bank( previous_bank = self._get_previous_bank(line)
cr, uid, line, context=context)
if previous_bank or not line.mandate_id.sepa_migrated: if previous_bank or not line.mandate_id.sepa_migrated:
amendment_indicator_2_50 = etree.SubElement( amendment_indicator_2_50 = etree.SubElement(
mandate_related_info_2_47, 'AmdmntInd') mandate_related_info_2_47, 'AmdmntInd')
@@ -322,13 +293,11 @@ class BankingExportSddWizard(orm.TransientModel):
ori_debtor_account_iban = etree.SubElement( ori_debtor_account_iban = etree.SubElement(
ori_debtor_account_id, 'IBAN') ori_debtor_account_id, 'IBAN')
ori_debtor_account_iban.text = self._validate_iban( ori_debtor_account_iban.text = self._validate_iban(
cr, uid, self._prepare_field( self._prepare_field(
cr, uid, 'Original Debtor Account', 'Original Debtor Account',
'previous_bank.acc_number', 'previous_bank.acc_number',
{'previous_bank': previous_bank}, {'previous_bank': previous_bank},
gen_args=gen_args, gen_args=gen_args))
context=context),
context=context)
else: else:
ori_debtor_agent_2_58 = etree.SubElement( ori_debtor_agent_2_58 = etree.SubElement(
amendment_info_details_2_51, 'OrgnlDbtrAgt') amendment_info_details_2_51, 'OrgnlDbtrAgt')
@@ -337,12 +306,11 @@ class BankingExportSddWizard(orm.TransientModel):
ori_debtor_agent_bic = etree.SubElement( ori_debtor_agent_bic = etree.SubElement(
ori_debtor_agent_institution, bic_xml_tag) ori_debtor_agent_institution, bic_xml_tag)
ori_debtor_agent_bic.text = self._prepare_field( ori_debtor_agent_bic.text = self._prepare_field(
cr, uid, 'Original Debtor Agent', 'Original Debtor Agent',
'previous_bank.bank.bic or ' 'previous_bank.bank.bic or '
'previous_bank.bank_bic', 'previous_bank.bank_bic',
{'previous_bank': previous_bank}, {'previous_bank': previous_bank},
gen_args=gen_args, gen_args=gen_args)
context=context)
ori_debtor_agent_other = etree.SubElement( ori_debtor_agent_other = etree.SubElement(
ori_debtor_agent_institution, 'Othr') ori_debtor_agent_institution, 'Othr')
ori_debtor_agent_other_id = etree.SubElement( ori_debtor_agent_other_id = etree.SubElement(
@@ -354,74 +322,73 @@ class BankingExportSddWizard(orm.TransientModel):
amendment_info_details_2_51, 'OrgnlMndtId') amendment_info_details_2_51, 'OrgnlMndtId')
ori_mandate_identification_2_52.text = \ ori_mandate_identification_2_52.text = \
self._prepare_field( self._prepare_field(
cr, uid, 'Original Mandate Identification', 'Original Mandate Identification',
'line.mandate_id.' 'line.mandate_id.'
'original_mandate_identification', 'original_mandate_identification',
{'line': line}, {'line': line},
gen_args=gen_args, gen_args=gen_args)
context=context)
ori_creditor_scheme_id_2_53 = etree.SubElement( ori_creditor_scheme_id_2_53 = etree.SubElement(
amendment_info_details_2_51, 'OrgnlCdtrSchmeId') amendment_info_details_2_51, 'OrgnlCdtrSchmeId')
self.generate_creditor_scheme_identification( self.generate_creditor_scheme_identification(
cr, uid, ori_creditor_scheme_id_2_53, ori_creditor_scheme_id_2_53,
'sepa_export.payment_order_ids[0].company_id.' 'self.payment_order_ids[0].company_id.'
'original_creditor_identifier', 'original_creditor_identifier',
'Original Creditor Identifier', 'Original Creditor Identifier',
{'sepa_export': sepa_export}, {'self': self}, 'SEPA', gen_args)
'SEPA', gen_args, context=context)
self.generate_party_block( self.generate_party_block(
cr, uid, dd_transaction_info_2_28, 'Dbtr', 'C', dd_transaction_info_2_28, 'Dbtr', 'C',
'line.partner_id.name', 'line.partner_id.name',
'line.bank_id.acc_number', 'line.bank_id.acc_number',
'line.bank_id.bank.bic or ' 'line.bank_id.bank.bic or '
'line.bank_id.bank_bic', 'line.bank_id.bank_bic',
{'line': line}, gen_args, context=context) {'line': line}, gen_args)
self.generate_remittance_info_block( self.generate_remittance_info_block(
cr, uid, dd_transaction_info_2_28, dd_transaction_info_2_28, line, gen_args)
line, gen_args, context=context)
nb_of_transactions_2_4.text = str(transactions_count_2_4) nb_of_transactions_2_4.text = unicode(transactions_count_2_4)
control_sum_2_5.text = '%.2f' % amount_control_sum_2_5 control_sum_2_5.text = '%.2f' % amount_control_sum_2_5
nb_of_transactions_1_6.text = str(transactions_count_1_6) nb_of_transactions_1_6.text = unicode(transactions_count_1_6)
control_sum_1_7.text = '%.2f' % amount_control_sum_1_7 control_sum_1_7.text = '%.2f' % amount_control_sum_1_7
return self.finalize_sepa_file_creation( return self.finalize_sepa_file_creation(
cr, uid, ids, xml_root, total_amount, transactions_count_1_6, xml_root, total_amount, transactions_count_1_6, gen_args)
gen_args, context=context)
def save_sepa(self, cr, uid, ids, context=None): @api.multi
def save_sepa(self):
"""Save the SEPA Direct Debit file: mark all payments in the file """Save the SEPA Direct Debit file: mark all payments in the file
as 'sent'. Write 'last debit date' on mandate and set oneoff as 'sent'. Write 'last debit date' on mandate and set oneoff
mandate to expired. mandate to expired.
""" """
sepa_export = self.browse(cr, uid, ids[0], context=context) abmo = self.env['account.banking.mandate']
for order in sepa_export.payment_order_ids: for order in self.payment_order_ids:
workflow.trg_validate(uid, 'payment.order', order.id, 'done', cr) workflow.trg_validate(
self.pool['ir.attachment'].create( self._uid, 'payment.order', order.id, 'done', self._cr)
cr, uid, { self.env['ir.attachment'].create({
'res_model': 'payment.order', 'res_model': 'payment.order',
'res_id': order.id, 'res_id': order.id,
'name': sepa_export.filename, 'name': self.filename,
'datas': sepa_export.file, 'datas': self.file,
}, context=context) })
to_expire_ids = [] to_expire_mandates = abmo.browse([])
first_mandate_ids = [] first_mandates = abmo.browse([])
all_mandates = abmo.browse([])
for line in order.line_ids: for line in order.line_ids:
all_mandates += line.mandate_id
if line.mandate_id.type == 'oneoff': if line.mandate_id.type == 'oneoff':
to_expire_ids.append(line.mandate_id.id) to_expire_mandates += line.mandate_id
elif line.mandate_id.type == 'recurrent': elif line.mandate_id.type == 'recurrent':
seq_type = line.mandate_id.recurrent_sequence_type seq_type = line.mandate_id.recurrent_sequence_type
if seq_type == 'final': if seq_type == 'final':
to_expire_ids.append(line.mandate_id.id) to_expire_mandates += line.mandate_id
elif seq_type == 'first': elif seq_type == 'first':
first_mandate_ids.append(line.mandate_id.id) first_mandates += line.mandate_id
self.pool['account.banking.mandate'].write( all_mandates.write(
cr, uid, to_expire_ids, {'state': 'expired'}, context=context) {'last_debit_date': fields.Date.context_today(self)})
self.pool['account.banking.mandate'].write( to_expire_mandates.write({'state': 'expired'})
cr, uid, first_mandate_ids, { first_mandates.write({
'recurrent_sequence_type': 'recurring', 'recurrent_sequence_type': 'recurring',
'sepa_migrated': True, 'sepa_migrated': True,
}, context=context) })
return True return True