diff --git a/account_banking_sepa_direct_debit/README.rst b/account_banking_sepa_direct_debit/README.rst index 04d40f4c3..22f07a8ad 100644 --- a/account_banking_sepa_direct_debit/README.rst +++ b/account_banking_sepa_direct_debit/README.rst @@ -1,6 +1,7 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg :alt: License: AGPL-3 +================================= Account Banking SEPA Direct Debit ================================= @@ -23,8 +24,8 @@ Installation ============ This module depends on : -* account_direct_debit -* account_banking_pain_base', + +* account_banking_pain_base * account_banking_mandate This module is part of the OCA/bank-payment suite. @@ -32,18 +33,21 @@ This module is part of the OCA/bank-payment suite. Configuration ============= -To configure this module, you need to: - - * Create a payment mode and select an export type related to debit order ( eg. "SEPA direct debit ...") +Create a Payment Mode dedicated to SEPA Direct Debit and select the +Payment Method *SEPA Direct Debit for customers* (which is automatically +created upon module installation) and check that this payment method +uses the proper version of PAIN. Usage ===== -To use this module, you must select this payment mode on a direct debit order (Menu :Accounting > Payment > Direct Debit orders) +In the menu *Accounting > Payments > Debit Order*, create a new debit +order and select the Payment Mode dedicated to SEPA Direct Debit that +you created during the configuration step. -For further information, please visit: - - * https://www.odoo.com/forum/help-1 +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/173/9.0 Known issues / Roadmap ====================== @@ -53,10 +57,10 @@ Known issues / Roadmap Bug Tracker =========== -Bugs are tracked on `GitHub Issues `_. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed feedback -`here `_. +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. Credits ======= @@ -64,7 +68,7 @@ Credits Contributors ------------ -* Alexis de Lattre +* Alexis de Lattre * Pedro M. Baeza * Stéphane Bidoul * Alexandre Fayolle diff --git a/account_banking_sepa_direct_debit/__init__.py b/account_banking_sepa_direct_debit/__init__.py index b4a69d367..a1815ae51 100644 --- a/account_banking_sepa_direct_debit/__init__.py +++ b/account_banking_sepa_direct_debit/__init__.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -# © 2013 Akretion (www.akretion.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import models -from . import wizard +from .post_install import update_bank_journals diff --git a/account_banking_sepa_direct_debit/__openerp__.py b/account_banking_sepa_direct_debit/__openerp__.py index 798db0176..d23eb7a93 100644 --- a/account_banking_sepa_direct_debit/__openerp__.py +++ b/account_banking_sepa_direct_debit/__openerp__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2013-2015 Akretion (www.akretion.com) +# © 2013-2016 Akretion (www.akretion.com) # © 2014 Serv. Tecnol. Avanzados - Pedro M. Baeza # © 2016 Antiun Ingenieria S.L. - Antonio Espinosa # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). @@ -7,7 +7,7 @@ { 'name': 'Account Banking SEPA Direct Debit', 'summary': 'Create SEPA files for Direct Debit', - 'version': '8.0.0.5.0', + 'version': '9.0.1.0.0', 'license': 'AGPL-3', 'author': "Akretion, " "Serv. Tecnol. Avanzados - Pedro M. Baeza, " @@ -16,22 +16,21 @@ 'website': 'https://github.com/OCA/bank-payment', 'category': 'Banking addons', 'depends': [ - 'account_direct_debit', 'account_banking_pain_base', 'account_banking_mandate', ], 'data': [ 'views/account_banking_mandate_view.xml', 'views/res_company_view.xml', - 'views/payment_mode_view.xml', - 'wizard/export_sdd_view.xml', + 'views/res_config.xml', + 'views/account_payment_mode.xml', 'data/mandate_expire_cron.xml', - 'data/payment_type_sdd.xml', + 'data/account_payment_method.xml', 'data/report_paperformat.xml', 'reports/sepa_direct_debit_mandate.xml', 'views/report_sepa_direct_debit_mandate.xml', - 'security/original_mandate_required_security.xml', ], 'demo': ['demo/sepa_direct_debit_demo.xml'], + 'post_init_hook': 'update_bank_journals', 'installable': True, } diff --git a/account_banking_sepa_direct_debit/data/account_payment_method.xml b/account_banking_sepa_direct_debit/data/account_payment_method.xml new file mode 100644 index 000000000..67a3339f6 --- /dev/null +++ b/account_banking_sepa_direct_debit/data/account_payment_method.xml @@ -0,0 +1,17 @@ + + + + + + + SEPA Direct Debit for customers + sepa_direct_debit + inbound + + + pain.008.001.02 + + + + + diff --git a/account_banking_sepa_direct_debit/data/mandate_expire_cron.xml b/account_banking_sepa_direct_debit/data/mandate_expire_cron.xml index 48fe6fc63..fc411dacf 100644 --- a/account_banking_sepa_direct_debit/data/mandate_expire_cron.xml +++ b/account_banking_sepa_direct_debit/data/mandate_expire_cron.xml @@ -1,7 +1,7 @@ diff --git a/account_banking_sepa_direct_debit/data/pain.008.003.02.xsd b/account_banking_sepa_direct_debit/data/pain.008.003.02.xsd new file mode 100644 index 000000000..ed2dd930b --- /dev/null +++ b/account_banking_sepa_direct_debit/data/pain.008.003.02.xsd @@ -0,0 +1,614 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mandatory if changes occur in ‘Mandate Identification’, otherwise not to be used. + + + + + Mandatory if changes occur in 'Creditor Scheme Identification', otherwise not to be used. + + + + + To be used only for changes of accounts within the same bank. + + + + + To use 'Identification’ under 'Other' under 'Financial Institution Identifier with code ‘SMNDA’ to indicate same mandate with new Debtor Agent. To be used with the ‘FRST’ indicator in the ‘Sequence Type’. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + If a Creditor Reference contains a check digit, the receiving bank is not required to validate this. +If the receiving bank validates the check digit and if this validation fails, the bank may continue its processing and send the transaction to the next party in the chain. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + It is recommended that all transactions within the same ‘Payment Information’ block have the same ‘Creditor Scheme Identification’. +This data element must be present at either ‘Payment Information’ or ‘Direct Debit +Transaction’ level. + + + + + + + + + + + It is recommended that this element be specified at ‘Payment Information’ level. + + + + + + This data element may be present either at ‘Payment Information’ or at ‘Direct Debit Transaction Information’ level. + + + + + + + + Mandatory if provided by the debtor in the mandate. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mandatory if 'Amendment Indicator' is 'TRUE' +The reason code from the Rulebook is indicated using one of the following message subelements. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Either ‘BIC or BEI’ or one +occurrence of ‘Other’ is allowed. + + + + + Either ‘Date and Place of Birth’ or one occurrence of ‘Other’ is allowed + + + + + + + + + + Private Identification is used to identify either an organisation or a private +person. + + + + + + + + + ‘Name’ is limited to 70 characters in length. + + + + + + + + + + ‘Name’ is limited to 70 characters in length. + + + + + + + + + + + + + + + + If present the new’ Name’ must be specified under ‘Creditor’. ‘Name’ is limited to 70 characters in length. + + + + + + + + + + ‘Name’ is limited to 70 characters in length. + + + + + + + + + + + + + + + + + + If present and contains ‘true’, batch booking is requested. If present and contains ‘false’, booking per transaction is requested. If element is not present, pre-agreed customer-to-bank conditions apply. + + + + + + + + + + + + This data element may be present either at ‘Payment Information’ or at ‘Direct Debit Transaction Information’ level. + + + + + It is recommended that this element be specified at ‘Payment Information’ level. + + + + + It is recommended that all transactions within the same ‘Payment Information’ block have the same ‘Creditor Scheme Identification’. +This data element must be present at either ‘Payment Information’ or ‘Direct Debit +Transaction’ level. + + + + + + + + + + + + + + + + Only ‘B2B’, 'CORE' or 'COR1' is allowed. The mixing of different Local Instrument values is not allowed in the same message. + + + + + If 'Amendment Indicator' is 'true' and 'Original Debtor Agent' is set to 'SMNDA' this message element must indicate 'FRST' + + + + + Depending on the agreement between the Creditor and the Creditor Bank, ‘Category Purpose’ may be forwarded to the Debtor Bank. + + + + + + + + + + + + + + + + + Only one occurrence of ‘Other’ is allowed, and no other sub-elements are allowed. +Identification must be used with an identifier described in General Message Element Specifications, Chapter 1.5.2 of the Implementation Guide. +Scheme Name’ under ‘Other’ must specify ‘SEPA’ under ‘Proprietary + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Only codes from the ISO 20022 ExternalPurposeCode list are allowed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + When present, the receiving bank is not obliged to validate the reference information. + + + + + + + + + + + + + + + + + + diff --git a/account_banking_sepa_direct_debit/data/payment_type_sdd.xml b/account_banking_sepa_direct_debit/data/payment_type_sdd.xml deleted file mode 100644 index a17b3b21c..000000000 --- a/account_banking_sepa_direct_debit/data/payment_type_sdd.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - SEPA Direct Debit v02 (recommended) - pain.008.001.02 - - - debit - - - - SEPA Direct Debit v03 - pain.008.001.03 - - - debit - - - - SEPA Direct Debit v04 - pain.008.001.04 - - - debit - - - - - diff --git a/account_banking_sepa_direct_debit/demo/sepa_direct_debit_demo.xml b/account_banking_sepa_direct_debit/demo/sepa_direct_debit_demo.xml index bcbeb7fb8..a93f87b3f 100644 --- a/account_banking_sepa_direct_debit/demo/sepa_direct_debit_demo.xml +++ b/account_banking_sepa_direct_debit/demo/sepa_direct_debit_demo.xml @@ -3,13 +3,11 @@ - - SEPA Direct Debit La Banque Postale - - + + SEPA Direct Debit of customers - - + variable + @@ -17,8 +15,9 @@ FR78ZZZ424242 + - + sepa recurrent first @@ -26,8 +25,14 @@ valid + + + + + + - + sepa recurrent first @@ -35,5 +40,10 @@ valid + + + + + diff --git a/account_banking_sepa_direct_debit/i18n/account_banking_sepa_direct_debit.pot b/account_banking_sepa_direct_debit/i18n/account_banking_sepa_direct_debit.pot index a39fa750a..834613460 100644 --- a/account_banking_sepa_direct_debit/i18n/account_banking_sepa_direct_debit.pot +++ b/account_banking_sepa_direct_debit/i18n/account_banking_sepa_direct_debit.pot @@ -319,11 +319,6 @@ msgstr "" msgid "SEPA Direct Debit Files" msgstr "" -#. module: account_banking_sepa_direct_debit -#: model:ir.actions.act_window,name:account_banking_sepa_direct_debit.mandate_action -msgid "SEPA Direct Debit Mandates" -msgstr "" - #. module: account_banking_sepa_direct_debit #: view:banking.export.sdd.wizard:account_banking_sepa_direct_debit.banking_export_sdd_wizard_view msgid "SEPA Direct Debit XML file generation" diff --git a/account_banking_sepa_direct_debit/i18n/fr.po b/account_banking_sepa_direct_debit/i18n/fr.po index 75cc33f0f..df7158fa4 100644 --- a/account_banking_sepa_direct_debit/i18n/fr.po +++ b/account_banking_sepa_direct_debit/i18n/fr.po @@ -402,11 +402,6 @@ msgstr "Mandats SEPA" msgid "SEPA Creditor Identifier" msgstr "Identifiant créancier SEPA" -#. module: account_banking_sepa_direct_debit -#: model:ir.actions.act_window,name:account_banking_sepa_direct_debit.mandate_action -msgid "SEPA Direct Debit Mandates" -msgstr "Mandats de prélèvement SEPA" - #. module: account_banking_sepa_direct_debit #: view:banking.export.sdd.wizard:account_banking_sepa_direct_debit.banking_export_sdd_wizard_view msgid "SEPA Direct Debit XML file generation" diff --git a/account_banking_sepa_direct_debit/models/__init__.py b/account_banking_sepa_direct_debit/models/__init__.py index 93fb91cc8..64b7c3874 100644 --- a/account_banking_sepa_direct_debit/models/__init__.py +++ b/account_banking_sepa_direct_debit/models/__init__.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- from . import res_company +from . import res_config from . import account_banking_mandate from . import bank_payment_line -from . import payment_mode +from . import account_payment_mode +from . import account_payment_method +from . import account_payment_order diff --git a/account_banking_sepa_direct_debit/models/account_banking_mandate.py b/account_banking_sepa_direct_debit/models/account_banking_mandate.py index ca2381204..744903cb6 100644 --- a/account_banking_sepa_direct_debit/models/account_banking_mandate.py +++ b/account_banking_sepa_direct_debit/models/account_banking_mandate.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2013 Akretion - Alexis de Lattre +# © 2013-2016 Akretion - Alexis de Lattre # © 2014 Serv. Tecnol. Avanzados - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). @@ -16,29 +16,14 @@ logger = logging.getLogger(__name__) class AccountBankingMandate(models.Model): """SEPA Direct Debit Mandate""" _inherit = 'account.banking.mandate' - _track = { - 'recurrent_sequence_type': { - 'account_banking_sepa_direct_debit.recurrent_sequence_type_first': - lambda self, cr, uid, obj, ctx=None: - obj['recurrent_sequence_type'] == 'first', - 'account_banking_sepa_direct_debit.' - 'recurrent_sequence_type_recurring': - lambda self, cr, uid, obj, ctx=None: - obj['recurrent_sequence_type'] == 'recurring', - 'account_banking_sepa_direct_debit.recurrent_sequence_type_final': - lambda self, cr, uid, obj, ctx=None: - obj['recurrent_sequence_type'] == 'final', - } - } + _rec_name = 'display_name' format = fields.Selection( - selection_add=[('sepa', _('Sepa Mandate'))], - default='sepa', - ) + selection_add=[('sepa', 'Sepa Mandate')], default='sepa') type = fields.Selection([('recurrent', 'Recurrent'), ('oneoff', 'One-Off')], string='Type of Mandate', - track_visibility='always') + track_visibility='onchange') recurrent_sequence_type = fields.Selection( [('first', 'First'), ('recurring', 'Recurring'), @@ -46,25 +31,12 @@ class AccountBankingMandate(models.Model): string='Sequence Type for Next Debit', track_visibility='onchange', help="This field is only used for Recurrent mandates, not for " "One-Off mandates.", default="first") - sepa_migrated = fields.Boolean( - string='Migrated to SEPA', track_visibility='onchange', - help="If this field is not active, the mandate section of the next " - "direct debit file that include this mandate will contain the " - "'Original Mandate Identification' and the 'Original Creditor " - "Scheme Identification'. This is required in a few countries " - "(Belgium for instance), but not in all countries. If this is " - "not required in your country, you should keep this field always " - "active.", default=True) - original_mandate_identification = fields.Char( - string='Original Mandate Identification', track_visibility='onchange', - size=35, - help="When the field 'Migrated to SEPA' is not active, this field " - "will be used as the Original Mandate Identification in the " - "Direct Debit file.") - scheme = fields.Selection([('CORE', 'Basic (CORE)'), - ('B2B', 'Enterprise (B2B)')], - string='Scheme', default="CORE") + scheme = fields.Selection([ + ('CORE', 'Basic (CORE)'), + ('B2B', 'Enterprise (B2B)')], + string='Scheme', default="CORE", track_visibility='onchange') unique_mandate_reference = fields.Char(size=35) # cf ISO 20022 + display_name = fields.Char(compute='compute_display_name', store=True) @api.multi @api.constrains('type', 'recurrent_sequence_type') @@ -77,28 +49,16 @@ class AccountBankingMandate(models.Model): % mandate.unique_mandate_reference) @api.multi - @api.constrains('type', 'recurrent_sequence_type', 'sepa_migrated') - def _check_migrated_to_sepa(self): + @api.depends('unique_mandate_reference', 'recurrent_sequence_type') + def compute_display_name(self): for mandate in self: - if (mandate.type == 'recurrent' and not mandate.sepa_migrated and - mandate.recurrent_sequence_type != 'first'): - raise exceptions.Warning( - _("The recurrent mandate '%s' which is not marked as " - "'Migrated to SEPA' must have its recurrent sequence " - "type set to 'First'.") - % mandate.unique_mandate_reference) - - @api.multi - @api.constrains('type', 'original_mandate_identification', 'sepa_migrated') - def _check_original_mandate_identification(self): - for mandate in self: - if (mandate.type == 'recurrent' and not mandate.sepa_migrated and - not mandate.original_mandate_identification): - raise exceptions.Warning( - _("You must set the 'Original Mandate Identification' on " - "the recurrent mandate '%s' which is not marked as " - "'Migrated to SEPA'.") - % mandate.unique_mandate_reference) + if mandate.format == 'sepa': + name = '%s (%s)' % ( + mandate.unique_mandate_reference, + mandate.recurrent_sequence_type) + else: + name = mandate.unique_mandate_reference + mandate.display_name = name @api.multi @api.onchange('partner_bank_id') @@ -137,5 +97,5 @@ class AccountBankingMandate(models.Model): 'The following SDD Mandate IDs has been set to expired: %s' % expired_mandates.ids) else: - logger.info('0 SDD Mandates must be set to Expired') + logger.info('0 SDD Mandates had to be set to Expired') return True diff --git a/account_banking_sepa_direct_debit/models/account_payment_method.py b/account_banking_sepa_direct_debit/models/account_payment_method.py new file mode 100644 index 000000000..c6781cc4f --- /dev/null +++ b/account_banking_sepa_direct_debit/models/account_payment_method.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, fields, api + + +class AccountPaymentMethod(models.Model): + _inherit = 'account.payment.method' + + pain_version = fields.Selection(selection_add=[ + ('pain.008.001.02', 'pain.008.001.02 (recommended for direct debit)'), + ('pain.008.001.03', 'pain.008.001.03'), + ('pain.008.001.04', 'pain.008.001.04'), + ('pain.008.003.02', 'pain.008.003.02 (direct debit in Germany)'), + ]) + + @api.multi + def get_xsd_file_path(self): + self.ensure_one() + if self.pain_version in [ + 'pain.008.001.02', 'pain.008.001.03', 'pain.008.001.04', + 'pain.008.003.02']: + path = 'account_banking_sepa_direct_debit/data/%s.xsd'\ + % self.pain_version + return path + return super(AccountPaymentMethod, self).get_xsd_file_path() diff --git a/account_banking_sepa_direct_debit/models/payment_mode.py b/account_banking_sepa_direct_debit/models/account_payment_mode.py similarity index 70% rename from account_banking_sepa_direct_debit/models/payment_mode.py rename to account_banking_sepa_direct_debit/models/account_payment_mode.py index f3c1ac1b6..0650736d0 100644 --- a/account_banking_sepa_direct_debit/models/payment_mode.py +++ b/account_banking_sepa_direct_debit/models/account_payment_mode.py @@ -2,12 +2,13 @@ # © 2016 Antiun Ingenieria S.L. - Antonio Espinosa # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, fields, api, exceptions, _ +from openerp import models, fields, api, _ from .common import is_sepa_creditor_identifier_valid +from openerp.exceptions import ValidationError -class PaymentMode(models.Model): - _inherit = 'payment.mode' +class AccountPaymentMode(models.Model): + _inherit = 'account.payment.mode' sepa_creditor_identifier = fields.Char( string='SEPA Creditor Identifier', size=35, @@ -19,13 +20,9 @@ class PaymentMode(models.Model): "- a 2-digits checkum\n" "- a 3-letters business code\n" "- a country-specific identifier") - original_creditor_identifier = fields.Char( - string='Original Creditor Identifier', size=70, - help="If not defined, Original Creditor Identifier from company " - "will be used.") def _sepa_type_get(self): - res = super(PaymentMode, self)._sepa_type_get() + res = super(AccountPaymentMode, self)._sepa_type_get() if not res: if self.type.code and self.type.code.startswith('pain.008'): res = 'sepa_direct_debit' @@ -38,6 +35,6 @@ class PaymentMode(models.Model): if payment_mode.sepa_creditor_identifier: if not is_sepa_creditor_identifier_valid( payment_mode.sepa_creditor_identifier): - raise exceptions.Warning( - _('Error'), - _("Invalid SEPA Creditor Identifier.")) + raise ValidationError( + _("The SEPA Creditor Identifier '%s' is invalid.") + % payment_mode.sepa_creditor_identifier) diff --git a/account_banking_sepa_direct_debit/models/account_payment_order.py b/account_banking_sepa_direct_debit/models/account_payment_order.py new file mode 100644 index 000000000..e68abe27e --- /dev/null +++ b/account_banking_sepa_direct_debit/models/account_payment_order.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields, api, _ +from openerp.exceptions import UserError +from lxml import etree + + +class AccountPaymentOrder(models.Model): + _inherit = 'account.payment.order' + + def _get_previous_bank(self, payline): + previous_bank = False + older_lines = self.env['account.payment.line'].search([ + ('mandate_id', '=', payline.mandate_id.id), + ('partner_bank_id', '!=', payline.partner_bank_id.id)]) + if older_lines: + previous_date = False + previous_payline = False + for older_line in older_lines: + if hasattr(older_line.order_id, 'date_sent'): + older_line_date = older_line.order_id.date_sent + else: + older_line_date = older_line.order_id.date_done + if (older_line_date and + older_line_date > previous_date): + previous_date = older_line_date + previous_payline = older_line + if previous_payline: + previous_bank = previous_payline.partner_bank_id + return previous_bank + + @api.multi + def generate_payment_file(self): + """Creates the SEPA Direct Debit file. That's the important code !""" + self.ensure_one() + if self.payment_method_id.code != 'sepa_direct_debit': + return super(AccountPaymentOrder, self).generate_payment_file() + pain_flavor = self.payment_method_id.pain_version + # We use pain_flavor.startswith('pain.008.001.xx') + # to support country-specific extensions such as + # pain.008.001.02.ch.01 (cf l10n_ch_sepa) + if pain_flavor.startswith('pain.008.001.02'): + bic_xml_tag = 'BIC' + name_maxsize = 70 + root_xml_tag = 'CstmrDrctDbtInitn' + elif pain_flavor.startswith('pain.008.003.02'): + bic_xml_tag = 'BIC' + name_maxsize = 70 + root_xml_tag = 'CstmrDrctDbtInitn' + elif pain_flavor.startswith('pain.008.001.03'): + bic_xml_tag = 'BICFI' + name_maxsize = 140 + root_xml_tag = 'CstmrDrctDbtInitn' + elif pain_flavor.startswith('pain.008.001.04'): + bic_xml_tag = 'BICFI' + name_maxsize = 140 + root_xml_tag = 'CstmrDrctDbtInitn' + else: + raise UserError( + _("Payment Type Code '%s' is not supported. The only " + "Payment Type Code supported for SEPA Direct Debit are " + "'pain.008.001.02', 'pain.008.001.03' and " + "'pain.008.001.04'.") % pain_flavor) + pay_method = self.payment_mode_id.payment_method_id + xsd_file = pay_method.get_xsd_file_path() + gen_args = { + 'bic_xml_tag': bic_xml_tag, + 'name_maxsize': name_maxsize, + 'convert_to_ascii': pay_method.convert_to_ascii, + 'payment_method': 'DD', + 'file_prefix': 'sdd_', + 'pain_flavor': pain_flavor, + 'pain_xsd_file': xsd_file, + } + nsmap = self.generate_pain_nsmap() + attrib = self.generate_pain_attrib() + xml_root = etree.Element('Document', nsmap=nsmap, attrib=attrib) + pain_root = etree.SubElement(xml_root, root_xml_tag) + # A. Group header + group_header, nb_of_transactions_a, control_sum_a = \ + self.generate_group_header_block(pain_root, gen_args) + transactions_count_a = 0 + amount_control_sum_a = 0.0 + lines_per_group = {} + # key = (requested_date, priority, sequence type) + # value = list of lines as objects + for line in self.bank_line_ids: + transactions_count_a += 1 + priority = line.priority + # The field line.date is the requested payment date + # taking into account the 'date_prefered' setting + # cf account_banking_payment_export/models/account_payment.py + # in the inherit of action_open() + if not line.mandate_id: + raise UserError( + _("Missing SEPA Direct Debit mandate on the " + "bank payment line with partner '%s' " + "(reference '%s').") + % (line.partner_id.name, line.name)) + scheme = line.mandate_id.scheme + if line.mandate_id.state != 'valid': + raise Warning( + _("The SEPA Direct Debit mandate with reference '%s' " + "for partner '%s' has expired.") + % (line.mandate_id.unique_mandate_reference, + line.mandate_id.partner_id.name)) + if line.mandate_id.type == 'oneoff': + seq_type = 'OOFF' + if line.mandate_id.last_debit_date: + raise Warning( + _("The mandate with reference '%s' for partner " + "'%s' has type set to 'One-Off' and it has a " + "last debit date set to '%s', so we can't use " + "it.") + % (line.mandate_id.unique_mandate_reference, + line.mandate_id.partner_id.name, + line.mandate_id.last_debit_date)) + elif line.mandate_id.type == 'recurrent': + seq_type_map = { + 'recurring': 'RCUR', + 'first': 'FRST', + 'final': 'FNAL', + } + seq_type_label = \ + line.mandate_id.recurrent_sequence_type + assert seq_type_label is not False + seq_type = seq_type_map[seq_type_label] + key = (line.date, priority, seq_type, scheme) + if key in lines_per_group: + lines_per_group[key].append(line) + else: + lines_per_group[key] = [line] + + for (requested_date, priority, sequence_type, scheme), lines in \ + lines_per_group.items(): + # B. Payment info + payment_info, nb_of_transactions_b, control_sum_b = \ + self.generate_start_payment_info_block( + pain_root, + "self.name + '-' + " + "sequence_type + '-' + requested_date.replace('-', '') " + "+ '-' + priority", + priority, scheme, sequence_type, requested_date, { + 'self': self, + 'sequence_type': sequence_type, + 'priority': priority, + 'requested_date': requested_date, + }, gen_args) + + self.generate_party_block( + payment_info, 'Cdtr', 'B', + self.company_partner_bank_id, gen_args) + charge_bearer = etree.SubElement(payment_info, 'ChrgBr') + if self.sepa: + charge_bearer_text = 'SLEV' + else: + charge_bearer_text = self.charge_bearer + charge_bearer.text = charge_bearer_text + creditor_scheme_identification = etree.SubElement( + payment_info, 'CdtrSchmeId') + self.generate_creditor_scheme_identification( + creditor_scheme_identification, + 'self.payment_mode_id.sepa_creditor_identifier or ' + 'self.company_id.sepa_creditor_identifier', + 'SEPA Creditor Identifier', {'self': self}, 'SEPA', gen_args) + transactions_count_b = 0 + amount_control_sum_b = 0.0 + for line in lines: + transactions_count_b += 1 + # C. Direct Debit Transaction Info + dd_transaction_info = etree.SubElement( + payment_info, 'DrctDbtTxInf') + payment_identification = etree.SubElement( + dd_transaction_info, 'PmtId') + if pain_flavor == 'pain.008.001.02.ch.01': + instruction_identification = etree.SubElement( + payment_identification, 'InstrId') + instruction_identification.text = self._prepare_field( + 'Intruction Identification', 'line.name', + {'line': line}, 35, gen_args=gen_args) + end2end_identification = etree.SubElement( + payment_identification, 'EndToEndId') + end2end_identification.text = self._prepare_field( + 'End to End Identification', 'line.name', + {'line': line}, 35, gen_args=gen_args) + currency_name = self._prepare_field( + 'Currency Code', 'line.currency_id.name', + {'line': line}, 3, gen_args=gen_args) + instructed_amount = etree.SubElement( + dd_transaction_info, 'InstdAmt', Ccy=currency_name) + instructed_amount.text = '%.2f' % line.amount_currency + amount_control_sum_a += line.amount_currency + amount_control_sum_b += line.amount_currency + dd_transaction = etree.SubElement( + dd_transaction_info, 'DrctDbtTx') + mandate_related_info = etree.SubElement( + dd_transaction, 'MndtRltdInf') + mandate_identification = etree.SubElement( + mandate_related_info, 'MndtId') + mandate_identification.text = self._prepare_field( + 'Unique Mandate Reference', + 'line.mandate_id.unique_mandate_reference', + {'line': line}, 35, gen_args=gen_args) + mandate_signature_date = etree.SubElement( + mandate_related_info, 'DtOfSgntr') + mandate_signature_date.text = self._prepare_field( + 'Mandate Signature Date', + 'line.mandate_id.signature_date', + {'line': line}, 10, gen_args=gen_args) + if sequence_type == 'FRST' and line.mandate_id.last_debit_date: + previous_bank = self._get_previous_bank(line) + if previous_bank: + amendment_indicator = etree.SubElement( + mandate_related_info, 'AmdmntInd') + amendment_indicator.text = 'true' + amendment_info_details = etree.SubElement( + mandate_related_info, 'AmdmntInfDtls') + if ( + previous_bank.bank_bic == + line.partner_bank_id.bank_bic): + ori_debtor_account = etree.SubElement( + amendment_info_details, 'OrgnlDbtrAcct') + ori_debtor_account_id = etree.SubElement( + ori_debtor_account, 'Id') + ori_debtor_account_iban = etree.SubElement( + ori_debtor_account_id, 'IBAN') + ori_debtor_account_iban.text = self._validate_iban( + self._prepare_field( + 'Original Debtor Account', + 'previous_bank.sanitized_acc_number', + {'previous_bank': previous_bank}, + gen_args=gen_args)) + else: + ori_debtor_agent = etree.SubElement( + amendment_info_details, 'OrgnlDbtrAgt') + ori_debtor_agent_institution = etree.SubElement( + ori_debtor_agent, 'FinInstnId') + ori_debtor_agent_bic = etree.SubElement( + ori_debtor_agent_institution, bic_xml_tag) + ori_debtor_agent_bic.text = self._prepare_field( + 'Original Debtor Agent', + 'previous_bank.bank_bic', + {'previous_bank': previous_bank}, + gen_args=gen_args) + ori_debtor_agent_other = etree.SubElement( + ori_debtor_agent_institution, 'Othr') + ori_debtor_agent_other_id = etree.SubElement( + ori_debtor_agent_other, 'Id') + ori_debtor_agent_other_id.text = 'SMNDA' + # SMNDA = Same Mandate New Debtor Agent + + self.generate_party_block( + dd_transaction_info, 'Dbtr', 'C', + line.partner_bank_id, gen_args, line) + + self.generate_remittance_info_block( + dd_transaction_info, line, gen_args) + + nb_of_transactions_b.text = unicode(transactions_count_b) + control_sum_b.text = '%.2f' % amount_control_sum_b + nb_of_transactions_a.text = unicode(transactions_count_a) + control_sum_a.text = '%.2f' % amount_control_sum_a + + return self.finalize_sepa_file_creation( + xml_root, gen_args) + + @api.multi + def finalize_sepa_file_creation(self, xml_root, gen_args): + """Save the SEPA Direct Debit file: mark all payments in the file + as 'sent'. Write 'last debit date' on mandate and set oneoff + mandate to expired. + """ + abmo = self.env['account.banking.mandate'] + to_expire_mandates = abmo.browse([]) + first_mandates = abmo.browse([]) + all_mandates = abmo.browse([]) + for bline in self.bank_line_ids: + if bline.mandate_id in all_mandates: + continue + all_mandates += bline.mandate_id + if bline.mandate_id.type == 'oneoff': + to_expire_mandates += bline.mandate_id + elif bline.mandate_id.type == 'recurrent': + seq_type = bline.mandate_id.recurrent_sequence_type + if seq_type == 'final': + to_expire_mandates += bline.mandate_id + elif seq_type == 'first': + first_mandates += bline.mandate_id + all_mandates.write( + {'last_debit_date': fields.Date.context_today(self)}) + to_expire_mandates.write({'state': 'expired'}) + first_mandates.write({ + 'recurrent_sequence_type': 'recurring', + }) + return super(AccountPaymentOrder, self).finalize_sepa_file_creation( + xml_root, gen_args) diff --git a/account_banking_sepa_direct_debit/models/bank_payment_line.py b/account_banking_sepa_direct_debit/models/bank_payment_line.py index e28207ebc..0321cf03d 100644 --- a/account_banking_sepa_direct_debit/models/bank_payment_line.py +++ b/account_banking_sepa_direct_debit/models/bank_payment_line.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2015 Akretion - Alexis de Lattre +# © 2015-2016 Akretion - Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from openerp import models, api @@ -9,7 +9,7 @@ class BankPaymentLine(models.Model): _inherit = 'bank.payment.line' @api.multi - def move_line_transfer_account_hashcode(self): + def move_line_offsetting_account_hashcode(self): """ From my experience, even when you ask several direct debits at the same date with enough delay, you will have several credits @@ -18,6 +18,6 @@ class BankPaymentLine(models.Model): reconciliation of the bank statement. """ hashcode = super(BankPaymentLine, self).\ - move_line_transfer_account_hashcode() + move_line_offsetting_account_hashcode() hashcode += '-' + unicode(self.mandate_id.recurrent_sequence_type) return hashcode diff --git a/account_banking_sepa_direct_debit/models/common.py b/account_banking_sepa_direct_debit/models/common.py index dd507a4db..33712f3b3 100644 --- a/account_banking_sepa_direct_debit/models/common.py +++ b/account_banking_sepa_direct_debit/models/common.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2013 Akretion (www.akretion.com) +# © 2013-2016 Akretion (Alexis de Lattre ) # © 2014 Serv. Tecnol. Avanzados - Pedro M. Baeza # © 2016 Antiun Ingenieria S.L. - Antonio Espinosa # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). @@ -21,7 +21,7 @@ def is_sepa_creditor_identifier_valid(sepa_creditor_identifier): sci = str(sepa_creditor_identifier).lower() except: logger.warning( - "SEPA Creditor ID should contain only ASCII caracters.") + "SEPA Creditor ID should contain only ASCII characters.") return False if len(sci) < 9: return False diff --git a/account_banking_sepa_direct_debit/models/res_company.py b/account_banking_sepa_direct_debit/models/res_company.py index c57dc8b04..ac58eb42f 100644 --- a/account_banking_sepa_direct_debit/models/res_company.py +++ b/account_banking_sepa_direct_debit/models/res_company.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- -# © 2013 Akretion (www.akretion.com) +# © 2013-2016 Akretion (Alexis de Lattre ) # © 2014 Serv. Tecnol. Avanzados - Pedro M. Baeza # © 2016 Antiun Ingenieria S.L. - Antonio Espinosa # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, fields, api, exceptions, _ +from openerp import models, fields, api, _ from .common import is_sepa_creditor_identifier_valid +from openerp.exceptions import ValidationError class ResCompany(models.Model): @@ -28,6 +29,6 @@ class ResCompany(models.Model): if company.sepa_creditor_identifier: if not is_sepa_creditor_identifier_valid( company.sepa_creditor_identifier): - raise exceptions.Warning( - _('Error'), - _("Invalid SEPA Creditor Identifier.")) + raise ValidationError( + _("The SEPA Creditor Identifier '%s' is invalid.") + % company.sepa_creditor_identifier) diff --git a/account_banking_sepa_direct_debit/models/res_config.py b/account_banking_sepa_direct_debit/models/res_config.py new file mode 100644 index 000000000..229e9fae0 --- /dev/null +++ b/account_banking_sepa_direct_debit/models/res_config.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, fields + + +class AccountConfigSettings(models.TransientModel): + _inherit = 'account.config.settings' + + sepa_creditor_identifier = fields.Char( + related='company_id.sepa_creditor_identifier') diff --git a/account_banking_sepa_direct_debit/post_install.py b/account_banking_sepa_direct_debit/post_install.py new file mode 100644 index 000000000..1fbd61793 --- /dev/null +++ b/account_banking_sepa_direct_debit/post_install.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import SUPERUSER_ID + + +def update_bank_journals(cr, pool): + ajo = pool['account.journal'] + journal_ids = ajo.search(cr, SUPERUSER_ID, [('type', '=', 'bank')]) + sdd_id = pool['ir.model.data'].xmlid_to_res_id( + cr, SUPERUSER_ID, + 'account_banking_sepa_direct_debit.sepa_direct_debit') + if sdd_id: + ajo.write(cr, SUPERUSER_ID, journal_ids, { + 'inbound_payment_method_ids': [(4, sdd_id)], + }) + return diff --git a/account_banking_sepa_direct_debit/security/original_mandate_required_security.xml b/account_banking_sepa_direct_debit/security/original_mandate_required_security.xml deleted file mode 100644 index e6a8570cf..000000000 --- a/account_banking_sepa_direct_debit/security/original_mandate_required_security.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - Original Mandate Required (SEPA) - - - - - diff --git a/account_banking_sepa_direct_debit/tests/__init__.py b/account_banking_sepa_direct_debit/tests/__init__.py new file mode 100644 index 000000000..27fbb4e38 --- /dev/null +++ b/account_banking_sepa_direct_debit/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_sdd diff --git a/account_banking_sepa_direct_debit/tests/test_sdd.py b/account_banking_sepa_direct_debit/tests/test_sdd.py new file mode 100644 index 000000000..c7df49b07 --- /dev/null +++ b/account_banking_sepa_direct_debit/tests/test_sdd.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp.addons.account.tests.account_test_classes\ + import AccountingTestCase +from openerp.tools import float_compare +import time +from lxml import etree + + +class TestSDD(AccountingTestCase): + + def test_sdd(self): + self.company = self.env['res.company'] + self.account_model = self.env['account.account'] + self.move_model = self.env['account.move'] + self.journal_model = self.env['account.journal'] + self.payment_order_model = self.env['account.payment.order'] + self.payment_line_model = self.env['account.payment.line'] + self.mandate_model = self.env['account.banking.mandate'] + self.bank_line_model = self.env['bank.payment.line'] + self.partner_bank_model = self.env['res.partner.bank'] + self.attachment_model = self.env['ir.attachment'] + self.invoice_model = self.env['account.invoice'] + self.invoice_line_model = self.env['account.invoice.line'] + company = self.env.ref('base.main_company') + self.partner_agrolait = self.env.ref('base.res_partner_2') + self.partner_c2c = self.env.ref('base.res_partner_12') + self.account_revenue = self.account_model.search([( + 'user_type_id', + '=', + self.env.ref('account.data_account_type_revenue').id)], limit=1) + self.account_receivable = self.account_model.search([( + 'user_type_id', + '=', + self.env.ref('account.data_account_type_receivable').id)], limit=1) + # create journal + self.bank_journal = self.journal_model.create({ + 'name': 'Company Bank journal', + 'type': 'bank', + 'code': 'BNKFC', + 'bank_account_id': + self.env.ref('account_payment_mode.main_company_iban').id, + 'bank_id': + self.env.ref('account_payment_mode.bank_la_banque_postale').id, + }) + # update payment mode + self.payment_mode = self.env.ref( + 'account_banking_sepa_direct_debit.' + 'payment_mode_inbound_sepa_dd1') + self.payment_mode.write({ + 'bank_account_link': 'fixed', + 'fixed_journal_id': self.bank_journal.id, + }) + eur_currency_id = self.env.ref('base.EUR').id + company.currency_id = eur_currency_id + self.env.ref( + 'account_banking_sepa_direct_debit.res_partner_2_mandate').\ + recurrent_sequence_type = 'first' + invoice1 = self.create_invoice( + self.partner_agrolait.id, + 'account_banking_sepa_direct_debit.res_partner_2_mandate', 42.0) + invoice2 = self.create_invoice( + self.partner_c2c.id, + 'account_banking_sepa_direct_debit.res_partner_12_mandate', 11.0) + for inv in [invoice1, invoice2]: + action = inv.create_account_payment_line() + self.assertEquals(action['res_model'], 'account.payment.order') + self.payment_order = self.payment_order_model.browse(action['res_id']) + self.assertEquals( + self.payment_order.payment_type, 'inbound') + self.assertEquals( + self.payment_order.payment_mode_id, self.payment_mode) + self.assertEquals( + self.payment_order.journal_id, self.bank_journal) + # Check payment line + pay_lines = self.payment_line_model.search([ + ('partner_id', '=', self.partner_agrolait.id), + ('order_id', '=', self.payment_order.id)]) + self.assertEquals(len(pay_lines), 1) + agrolait_pay_line1 = pay_lines[0] + accpre = self.env['decimal.precision'].precision_get('Account') + self.assertEquals(agrolait_pay_line1.currency_id.id, eur_currency_id) + self.assertEquals( + agrolait_pay_line1.mandate_id, invoice1.mandate_id) + self.assertEquals( + agrolait_pay_line1.partner_bank_id, + invoice1.mandate_id.partner_bank_id) + self.assertEquals(float_compare( + agrolait_pay_line1.amount_currency, 42, precision_digits=accpre), + 0) + self.assertEquals(agrolait_pay_line1.communication_type, 'normal') + self.assertEquals(agrolait_pay_line1.communication, invoice1.number) + self.payment_order.draft2open() + self.assertEquals(self.payment_order.state, 'open') + self.assertEquals(self.payment_order.sepa, True) + # Check bank payment line + bank_lines = self.bank_line_model.search([ + ('partner_id', '=', self.partner_agrolait.id)]) + self.assertEquals(len(bank_lines), 1) + agrolait_bank_line = bank_lines[0] + self.assertEquals(agrolait_bank_line.currency_id.id, eur_currency_id) + self.assertEquals(float_compare( + agrolait_bank_line.amount_currency, 42.0, precision_digits=accpre), + 0) + self.assertEquals(agrolait_bank_line.communication_type, 'normal') + self.assertEquals( + agrolait_bank_line.communication, invoice1.number) + self.assertEquals( + agrolait_bank_line.mandate_id, invoice1.mandate_id) + self.assertEquals( + agrolait_bank_line.partner_bank_id, + invoice1.mandate_id.partner_bank_id) + action = self.payment_order.open2generated() + self.assertEquals(self.payment_order.state, 'generated') + self.assertEquals(action['res_model'], 'ir.attachment') + attachment = self.attachment_model.browse(action['res_id']) + self.assertEquals(attachment.datas_fname[-4:], '.xml') + xml_file = attachment.datas.decode('base64') + xml_root = etree.fromstring(xml_file) + # print "xml_file=", etree.tostring(xml_root, pretty_print=True) + namespaces = xml_root.nsmap + namespaces['p'] = xml_root.nsmap[None] + namespaces.pop(None) + pay_method_xpath = xml_root.xpath( + '//p:PmtInf/p:PmtMtd', namespaces=namespaces) + self.assertEquals(pay_method_xpath[0].text, 'DD') + sepa_xpath = xml_root.xpath( + '//p:PmtInf/p:PmtTpInf/p:SvcLvl/p:Cd', namespaces=namespaces) + self.assertEquals(sepa_xpath[0].text, 'SEPA') + debtor_acc_xpath = xml_root.xpath( + '//p:PmtInf/p:CdtrAcct/p:Id/p:IBAN', namespaces=namespaces) + self.assertEquals( + debtor_acc_xpath[0].text, + self.payment_order.company_partner_bank_id.sanitized_acc_number) + self.payment_order.generated2uploaded() + self.assertEquals(self.payment_order.state, 'uploaded') + for inv in [invoice1, invoice2]: + self.assertEquals(inv.state, 'paid') + self.assertEquals(self.env.ref( + 'account_banking_sepa_direct_debit.res_partner_2_mandate'). + recurrent_sequence_type, 'recurring') + return + + def create_invoice( + self, partner_id, mandate_xmlid, price_unit, type='out_invoice'): + invoice = self.invoice_model.create({ + 'partner_id': partner_id, + 'reference_type': 'none', + 'currency_id': self.env.ref('base.EUR').id, + 'name': 'test', + 'account_id': self.account_receivable.id, + 'type': type, + 'date_invoice': time.strftime('%Y-%m-%d'), + 'payment_mode_id': self.payment_mode.id, + 'mandate_id': self.env.ref(mandate_xmlid).id, + }) + self.invoice_line_model.create({ + 'invoice_id': invoice.id, + 'price_unit': price_unit, + 'quantity': 1, + 'name': 'Great service', + 'account_id': self.account_revenue.id, + }) + invoice.signal_workflow('invoice_open') + return invoice diff --git a/account_banking_sepa_direct_debit/views/account_banking_mandate_view.xml b/account_banking_sepa_direct_debit/views/account_banking_mandate_view.xml index 667cedb54..c27dcb175 100644 --- a/account_banking_sepa_direct_debit/views/account_banking_mandate_view.xml +++ b/account_banking_sepa_direct_debit/views/account_banking_mandate_view.xml @@ -1,6 +1,6 @@ @@ -11,41 +11,37 @@ - + sdd.mandate.form account.banking.mandate + - - - - - - + sdd.mandate.tree account.banking.mandate - + - + sdd.mandate.search account.banking.mandate @@ -66,67 +62,5 @@ - - SEPA Direct Debit Mandates - account.banking.mandate - tree,form - -

- Click to create a new SEPA Direct Debit Mandate. -

- A SEPA Direct Debit Mandate is a document signed by your customer that gives you the autorization to do one or several direct debits on his bank account. -

-
-
- - - - - sdd.mandate.res.partner.bank.tree - res.partner.bank - - - - SDD Mandates - - - - - - sdd.mandate.partner.form - res.partner - - - - SDD Mandates - - - - - - Sequence Type set to First - account.banking.mandate - - Sequence Type set to First - - - - Sequence Type set to Recurring - account.banking.mandate - - Sequence Type set to Recurring - - - - Sequence Type set to Final - account.banking.mandate - - Sequence Type set to Final - -
diff --git a/account_banking_sepa_direct_debit/views/account_payment_mode.xml b/account_banking_sepa_direct_debit/views/account_payment_mode.xml new file mode 100644 index 000000000..23eca685f --- /dev/null +++ b/account_banking_sepa_direct_debit/views/account_payment_mode.xml @@ -0,0 +1,23 @@ + + + + + + + Add SEPA identifiers on payment mode form + account.payment.mode + + + + + + + + + + + diff --git a/account_banking_sepa_direct_debit/views/payment_mode_view.xml b/account_banking_sepa_direct_debit/views/payment_mode_view.xml deleted file mode 100644 index 9bba891d6..000000000 --- a/account_banking_sepa_direct_debit/views/payment_mode_view.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - Add SEPA identifiers - payment.mode - - - - - - - - - - - - - diff --git a/account_banking_sepa_direct_debit/views/report_sepa_direct_debit_mandate.xml b/account_banking_sepa_direct_debit/views/report_sepa_direct_debit_mandate.xml index eb850815b..c71a2083b 100644 --- a/account_banking_sepa_direct_debit/views/report_sepa_direct_debit_mandate.xml +++ b/account_banking_sepa_direct_debit/views/report_sepa_direct_debit_mandate.xml @@ -3,6 +3,8 @@ diff --git a/account_banking_sepa_direct_debit/views/res_company_view.xml b/account_banking_sepa_direct_debit/views/res_company_view.xml index e4c9e0b93..6b4d544ca 100644 --- a/account_banking_sepa_direct_debit/views/res_company_view.xml +++ b/account_banking_sepa_direct_debit/views/res_company_view.xml @@ -1,20 +1,19 @@ - + sepa_direct_debit.res.company.form res.company - diff --git a/account_banking_sepa_direct_debit/views/res_config.xml b/account_banking_sepa_direct_debit/views/res_config.xml new file mode 100644 index 000000000..8e37d7eab --- /dev/null +++ b/account_banking_sepa_direct_debit/views/res_config.xml @@ -0,0 +1,21 @@ + + + + + + sepa_direct_debit.account_config_settings.form + account.config.settings + + +
+
+
+
+
+
+ +
+
diff --git a/account_banking_sepa_direct_debit/wizard/__init__.py b/account_banking_sepa_direct_debit/wizard/__init__.py deleted file mode 100644 index 3830e36d9..000000000 --- a/account_banking_sepa_direct_debit/wizard/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# SEPA Direct Debit module for OpenERP -# Copyright (C) 2013 Akretion (http://www.akretion.com) -# @author: Alexis de Lattre -# -# 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 . -# -############################################################################## - -from . import export_sdd diff --git a/account_banking_sepa_direct_debit/wizard/export_sdd.py b/account_banking_sepa_direct_debit/wizard/export_sdd.py deleted file mode 100644 index 9b4022d46..000000000 --- a/account_banking_sepa_direct_debit/wizard/export_sdd.py +++ /dev/null @@ -1,394 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# SEPA Direct Debit module for Odoo -# Copyright (C) 2013-2015 Akretion (http://www.akretion.com) -# @author: Alexis de Lattre -# -# 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 . -# -############################################################################## - - -from openerp import models, fields, api, _ -from openerp.exceptions import Warning -from openerp import workflow -from lxml import etree - - -class BankingExportSddWizard(models.TransientModel): - _name = 'banking.export.sdd.wizard' - _inherit = ['banking.export.pain'] - _description = 'Export SEPA Direct Debit File' - - state = fields.Selection([ - ('create', '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) - - @api.model - def create(self, vals): - payment_order_ids = self._context.get('active_ids', []) - vals.update({ - 'payment_order_ids': [[6, 0, payment_order_ids]], - }) - return super(BankingExportSddWizard, self).create(vals) - - def _get_previous_bank(self, payline): - previous_bank = False - older_lines = self.env['payment.line'].search([ - ('mandate_id', '=', payline.mandate_id.id), - ('bank_id', '!=', payline.bank_id.id)]) - if older_lines: - previous_date = False - previous_payline = False - for older_line in older_lines: - if hasattr(older_line.order_id, 'date_sent'): - older_line_date = older_line.order_id.date_sent - else: - older_line_date = older_line.order_id.date_done - if (older_line_date and - older_line_date > previous_date): - previous_date = older_line_date - previous_payline = older_line - if previous_payline: - previous_bank = previous_payline.bank_id - return previous_bank - - @api.multi - def create_sepa(self): - """Creates the SEPA Direct Debit file. That's the important code !""" - pain_flavor = self.payment_order_ids[0].mode.type.code - convert_to_ascii = \ - self.payment_order_ids[0].mode.convert_to_ascii - if pain_flavor == 'pain.008.001.02': - bic_xml_tag = 'BIC' - name_maxsize = 70 - root_xml_tag = 'CstmrDrctDbtInitn' - elif pain_flavor == 'pain.008.001.03': - bic_xml_tag = 'BICFI' - name_maxsize = 140 - root_xml_tag = 'CstmrDrctDbtInitn' - elif pain_flavor == 'pain.008.001.04': - bic_xml_tag = 'BICFI' - name_maxsize = 140 - root_xml_tag = 'CstmrDrctDbtInitn' - else: - raise Warning( - _("Payment Type Code '%s' is not supported. The only " - "Payment Type Code supported for SEPA Direct Debit are " - "'pain.008.001.02', 'pain.008.001.03' and " - "'pain.008.001.04'.") % pain_flavor) - gen_args = { - 'bic_xml_tag': bic_xml_tag, - 'name_maxsize': name_maxsize, - 'convert_to_ascii': convert_to_ascii, - 'payment_method': 'DD', - 'file_prefix': 'sdd_', - 'pain_flavor': pain_flavor, - 'pain_xsd_file': - 'account_banking_sepa_direct_debit/data/%s.xsd' % pain_flavor, - } - pain_ns = { - 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', - None: 'urn:iso:std:iso:20022:tech:xsd:%s' % pain_flavor, - } - xml_root = etree.Element('Document', nsmap=pain_ns) - pain_root = etree.SubElement(xml_root, root_xml_tag) - # A. Group header - group_header_1_0, nb_of_transactions_1_6, control_sum_1_7 = \ - self.generate_group_header_block(pain_root, gen_args) - transactions_count_1_6 = 0 - total_amount = 0.0 - amount_control_sum_1_7 = 0.0 - lines_per_group = {} - # key = (requested_date, priority, sequence type) - # value = list of lines as objects - # Iterate on payment orders - for payment_order in self.payment_order_ids: - total_amount = total_amount + payment_order.total - # Iterate each payment lines - for line in payment_order.bank_line_ids: - transactions_count_1_6 += 1 - priority = line.priority - # The field line.date is the requested payment date - # taking into account the 'date_prefered' setting - # cf account_banking_payment_export/models/account_payment.py - # in the inherit of action_open() - if not line.mandate_id: - raise Warning( - _("Missing SEPA Direct Debit mandate on the " - "bank payment line with partner '%s' " - "(reference '%s').") - % (line.partner_id.name, line.name)) - scheme = line.mandate_id.scheme - if line.mandate_id.state != 'valid': - raise Warning( - _("The SEPA Direct Debit mandate with reference '%s' " - "for partner '%s' has expired.") - % (line.mandate_id.unique_mandate_reference, - line.mandate_id.partner_id.name)) - if line.mandate_id.type == 'oneoff': - seq_type = 'OOFF' - if line.mandate_id.last_debit_date: - raise Warning( - _("The mandate with reference '%s' for partner " - "'%s' has type set to 'One-Off' and it has a " - "last debit date set to '%s', so we can't use " - "it.") - % (line.mandate_id.unique_mandate_reference, - line.mandate_id.partner_id.name, - line.mandate_id.last_debit_date)) - elif line.mandate_id.type == 'recurrent': - seq_type_map = { - 'recurring': 'RCUR', - 'first': 'FRST', - 'final': 'FNAL', - } - seq_type_label = \ - line.mandate_id.recurrent_sequence_type - assert seq_type_label is not False - seq_type = seq_type_map[seq_type_label] - key = (line.date, priority, seq_type, scheme) - if key in lines_per_group: - lines_per_group[key].append(line) - else: - lines_per_group[key] = [line] - - for (requested_date, priority, sequence_type, scheme), lines in \ - lines_per_group.items(): - # B. Payment info - payment_info_2_0, nb_of_transactions_2_4, control_sum_2_5 = \ - self.generate_start_payment_info_block( - pain_root, - "self.payment_order_ids[0].reference + '-' + " - "sequence_type + '-' + requested_date.replace('-', '') " - "+ '-' + priority", - priority, scheme, sequence_type, requested_date, { - 'self': self, - 'sequence_type': sequence_type, - 'priority': priority, - 'requested_date': requested_date, - }, gen_args) - - self.generate_party_block( - payment_info_2_0, 'Cdtr', 'B', - 'self.payment_order_ids[0].mode.bank_id.partner_id.' - 'name', - 'self.payment_order_ids[0].mode.bank_id.acc_number', - 'self.payment_order_ids[0].mode.bank_id.bank.bic or ' - 'self.payment_order_ids[0].mode.bank_id.bank_bic', - {'self': self}, gen_args) - charge_bearer_2_24 = etree.SubElement(payment_info_2_0, 'ChrgBr') - charge_bearer_2_24.text = self.charge_bearer - creditor_scheme_identification_2_27 = etree.SubElement( - payment_info_2_0, 'CdtrSchmeId') - self.generate_creditor_scheme_identification( - creditor_scheme_identification_2_27, - 'self.payment_order_ids[0].mode.' - 'sepa_creditor_identifier or ' - 'self.payment_order_ids[0].company_id.' - 'sepa_creditor_identifier', - 'SEPA Creditor Identifier', {'self': self}, 'SEPA', gen_args) - transactions_count_2_4 = 0 - amount_control_sum_2_5 = 0.0 - for line in lines: - transactions_count_2_4 += 1 - # C. Direct Debit Transaction Info - dd_transaction_info_2_28 = etree.SubElement( - payment_info_2_0, 'DrctDbtTxInf') - payment_identification_2_29 = etree.SubElement( - dd_transaction_info_2_28, 'PmtId') - end2end_identification_2_31 = etree.SubElement( - payment_identification_2_29, 'EndToEndId') - end2end_identification_2_31.text = self._prepare_field( - 'End to End Identification', 'line.name', - {'line': line}, 35, gen_args=gen_args) - currency_name = self._prepare_field( - 'Currency Code', 'line.currency.name', - {'line': line}, 3, gen_args=gen_args) - instructed_amount_2_44 = etree.SubElement( - dd_transaction_info_2_28, 'InstdAmt', Ccy=currency_name) - instructed_amount_2_44.text = '%.2f' % line.amount_currency - amount_control_sum_1_7 += line.amount_currency - amount_control_sum_2_5 += line.amount_currency - dd_transaction_2_46 = etree.SubElement( - dd_transaction_info_2_28, 'DrctDbtTx') - mandate_related_info_2_47 = etree.SubElement( - dd_transaction_2_46, 'MndtRltdInf') - mandate_identification_2_48 = etree.SubElement( - mandate_related_info_2_47, 'MndtId') - mandate_identification_2_48.text = self._prepare_field( - 'Unique Mandate Reference', - 'line.mandate_id.unique_mandate_reference', - {'line': line}, 35, gen_args=gen_args) - mandate_signature_date_2_49 = etree.SubElement( - mandate_related_info_2_47, 'DtOfSgntr') - mandate_signature_date_2_49.text = self._prepare_field( - 'Mandate Signature Date', - 'line.mandate_id.signature_date', - {'line': line}, 10, gen_args=gen_args) - if sequence_type == 'FRST' and ( - line.mandate_id.last_debit_date or - not line.mandate_id.sepa_migrated): - previous_bank = self._get_previous_bank(line) - if previous_bank or not line.mandate_id.sepa_migrated: - amendment_indicator_2_50 = etree.SubElement( - mandate_related_info_2_47, 'AmdmntInd') - amendment_indicator_2_50.text = 'true' - amendment_info_details_2_51 = etree.SubElement( - mandate_related_info_2_47, 'AmdmntInfDtls') - if previous_bank: - if (previous_bank.bank.bic or - previous_bank.bank_bic) == \ - (line.bank_id.bank.bic or - line.bank_id.bank_bic): - ori_debtor_account_2_57 = etree.SubElement( - amendment_info_details_2_51, 'OrgnlDbtrAcct') - ori_debtor_account_id = etree.SubElement( - ori_debtor_account_2_57, 'Id') - ori_debtor_account_iban = etree.SubElement( - ori_debtor_account_id, 'IBAN') - ori_debtor_account_iban.text = self._validate_iban( - self._prepare_field( - 'Original Debtor Account', - 'previous_bank.acc_number', - {'previous_bank': previous_bank}, - gen_args=gen_args)) - else: - ori_debtor_agent_2_58 = etree.SubElement( - amendment_info_details_2_51, 'OrgnlDbtrAgt') - ori_debtor_agent_institution = etree.SubElement( - ori_debtor_agent_2_58, 'FinInstnId') - ori_debtor_agent_bic = etree.SubElement( - ori_debtor_agent_institution, bic_xml_tag) - ori_debtor_agent_bic.text = self._prepare_field( - 'Original Debtor Agent', - 'previous_bank.bank.bic or ' - 'previous_bank.bank_bic', - {'previous_bank': previous_bank}, - gen_args=gen_args) - ori_debtor_agent_other = etree.SubElement( - ori_debtor_agent_institution, 'Othr') - ori_debtor_agent_other_id = etree.SubElement( - ori_debtor_agent_other, 'Id') - ori_debtor_agent_other_id.text = 'SMNDA' - # SMNDA = Same Mandate New Debtor Agent - elif not line.mandate_id.sepa_migrated: - ori_mandate_identification_2_52 = etree.SubElement( - amendment_info_details_2_51, 'OrgnlMndtId') - ori_mandate_identification_2_52.text = \ - self._prepare_field( - 'Original Mandate Identification', - 'line.mandate_id.' - 'original_mandate_identification', - {'line': line}, - gen_args=gen_args) - ori_creditor_scheme_id_2_53 = etree.SubElement( - amendment_info_details_2_51, 'OrgnlCdtrSchmeId') - self.generate_creditor_scheme_identification( - ori_creditor_scheme_id_2_53, - 'self.payment_order_ids[0].mode.' - 'original_creditor_identifier or ' - 'self.payment_order_ids[0].company_id.' - 'original_creditor_identifier', - 'Original Creditor Identifier', - {'self': self}, 'SEPA', gen_args) - - self.generate_party_block( - dd_transaction_info_2_28, 'Dbtr', 'C', - 'line.partner_id.name', - 'line.bank_id.acc_number', - 'line.bank_id.bank.bic or ' - 'line.bank_id.bank_bic', - {'line': line}, gen_args) - - self.generate_remittance_info_block( - dd_transaction_info_2_28, line, gen_args) - - nb_of_transactions_2_4.text = unicode(transactions_count_2_4) - control_sum_2_5.text = '%.2f' % amount_control_sum_2_5 - nb_of_transactions_1_6.text = unicode(transactions_count_1_6) - control_sum_1_7.text = '%.2f' % amount_control_sum_1_7 - - return self.finalize_sepa_file_creation( - xml_root, total_amount, transactions_count_1_6, gen_args) - - @api.multi - def save_sepa(self): - """Save the SEPA Direct Debit file: mark all payments in the file - as 'sent'. Write 'last debit date' on mandate and set oneoff - mandate to expired. - """ - abmo = self.env['account.banking.mandate'] - for order in self.payment_order_ids: - workflow.trg_validate( - self._uid, 'payment.order', order.id, 'done', self._cr) - self.env['ir.attachment'].create({ - 'res_model': 'payment.order', - 'res_id': order.id, - 'name': self.filename, - 'datas': self.file, - }) - to_expire_mandates = abmo.browse([]) - first_mandates = abmo.browse([]) - all_mandates = abmo.browse([]) - for bline in order.bank_line_ids: - if bline.mandate_id in all_mandates: - continue - all_mandates += bline.mandate_id - if bline.mandate_id.type == 'oneoff': - to_expire_mandates += bline.mandate_id - elif bline.mandate_id.type == 'recurrent': - seq_type = bline.mandate_id.recurrent_sequence_type - if seq_type == 'final': - to_expire_mandates += bline.mandate_id - elif seq_type == 'first': - first_mandates += bline.mandate_id - all_mandates.write( - {'last_debit_date': fields.Date.context_today(self)}) - to_expire_mandates.write({'state': 'expired'}) - first_mandates.write({ - 'recurrent_sequence_type': 'recurring', - 'sepa_migrated': True, - }) - return True diff --git a/account_banking_sepa_direct_debit/wizard/export_sdd_view.xml b/account_banking_sepa_direct_debit/wizard/export_sdd_view.xml deleted file mode 100644 index 95117e270..000000000 --- a/account_banking_sepa_direct_debit/wizard/export_sdd_view.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - banking.export.sdd.wizard.view - banking.export.sdd.wizard - -
- - - - - - - - - - - - - -
-
- -
-