From 72cf6e47e50b4ff3ea1883e59ca9015c59257b15 Mon Sep 17 00:00:00 2001 From: aruseni Date: Wed, 23 Dec 2020 13:07:01 +0200 Subject: [PATCH 1/2] Nordea ISO 20022 --- frontend/src-admin/views/BankExportView.vue | 41 +++++-- .../src-admin/views/MarketSettingsView.vue | 108 +++++++++++++++++- frontend/src-base/configs/index.js | 9 ++ frontend/src-base/locale/da/da.json | 6 +- frontend/src-base/locale/en/en.json | 6 +- loppeonline/api/v1/admin/api.py | 5 +- loppeonline/api/v1/admin/serializers.py | 22 ++++ .../migrations/0020_auto_20201223_0031.py | 29 +++++ loppeonline/apps/markets/models.py | 21 ++++ .../apps/sales/bank_export/__init__.py | 2 + .../apps/sales/bank_export/bankdata.py | 2 + loppeonline/apps/sales/bank_export/bec.py | 2 + .../apps/sales/bank_export/danske_bank.py | 2 + loppeonline/apps/sales/bank_export/nordea.py | 2 + .../sales/bank_export/nordea_iso_20022.py | 43 +++++++ loppeonline/apps/sales/bank_export/sdc.py | 2 + .../apps/sales/templatetags/bank_export.py | 11 ++ .../bank_export/nordea_iso_20022.xml | 103 +++++++++++++++++ 18 files changed, 401 insertions(+), 15 deletions(-) create mode 100644 loppeonline/apps/markets/migrations/0020_auto_20201223_0031.py create mode 100644 loppeonline/apps/sales/bank_export/nordea_iso_20022.py create mode 100644 loppeonline/apps/sales/templatetags/bank_export.py create mode 100644 loppeonline/templates/bank_export/nordea_iso_20022.xml diff --git a/frontend/src-admin/views/BankExportView.vue b/frontend/src-admin/views/BankExportView.vue index a2d332c6..45505670 100644 --- a/frontend/src-admin/views/BankExportView.vue +++ b/frontend/src-admin/views/BankExportView.vue @@ -79,6 +79,23 @@ + + + + + { - saveFile(response.data['file'], filename, filetype) + const filename = this.$t( + 'pages.bank_export.filename', + { + filetype: this.bankFileFormatSingleLabel, + description: this.bankExportTransactions[0].receiver, + extension: response.data.extension, + } + ) + const filetypes = { + 'txt': 'text/plain', + 'xml': 'application/xml', + } + saveFile( + response.data.file, filename, filetypes[response.data.extension] + ) }) .catch((error) => { this.$_notifyError(error.response, this) diff --git a/frontend/src-admin/views/MarketSettingsView.vue b/frontend/src-admin/views/MarketSettingsView.vue index 1d68c37c..cb05d4d7 100644 --- a/frontend/src-admin/views/MarketSettingsView.vue +++ b/frontend/src-admin/views/MarketSettingsView.vue @@ -296,6 +296,40 @@ > + + + + + + + + + + - + - +

{{ $t('pages.market_settings.wrong_api_settings_title') }}

@@ -497,6 +531,8 @@ support_phone: '', bank_reg_number: '', bank_account: '', + nordea_agreement_number: '', + nordea_signer_id: '', bank_file_format: '', instagram_url: '', facebook_url: '', @@ -525,6 +561,10 @@ AVAILABLE_BANK_FORMATS, BANK_REG_NUMBER_LENGTH, MAX_BANK_ACCOUNT_LENGTH, + MIN_NORDEA_AGREEMENT_NUMBER_LENGTH, + MAX_NORDEA_AGREEMENT_NUMBER_LENGTH, + MIN_NORDEA_SIGNER_ID_LENGTH, + MAX_NORDEA_SIGNER_ID_LENGTH, } from '@base/configs' import HeadBlock from '@base/components/HeadBlock.vue' import ObjectUtils from '@base/mixins/ObjectUtils.vue' @@ -560,7 +600,7 @@ loading: false } }, - + validations: { phoneNumberData: { required @@ -599,6 +639,16 @@ numeric, maxLength: maxLength(MAX_BANK_ACCOUNT_LENGTH), }, + nordea_agreement_number: { + numeric, + minLength: minLength(MIN_NORDEA_AGREEMENT_NUMBER_LENGTH), + maxLength: maxLength(MAX_NORDEA_AGREEMENT_NUMBER_LENGTH), + }, + nordea_signer_id: { + numeric, + minLength: minLength(MIN_NORDEA_SIGNER_ID_LENGTH), + maxLength: maxLength(MAX_NORDEA_SIGNER_ID_LENGTH), + }, instagram_url: { instagramUrlValidator, }, @@ -663,6 +713,8 @@ bank_reg_number: this.marketSettingsForm.bank_reg_number, bank_account: this.marketSettingsForm.bank_account ? this.marketSettingsForm.bank_account.padStart(MAX_BANK_ACCOUNT_LENGTH, '0') : '', + nordea_agreement_number: this.marketSettingsForm.nordea_agreement_number, + nordea_signer_id: this.marketSettingsForm.nordea_signer_id, bank_file_format: this.marketSettingsForm.bank_file_format ? this.marketSettingsForm.bank_file_format.code : '', economic_agreement_token: this.marketSettingsForm.economic_agreement_token, flexpos_api_key: this.marketSettingsForm.flexpos_api_key, @@ -740,6 +792,56 @@ } return null }, + stateNordeaAgreementNumberInput () { + return this.$v.marketSettingsForm.nordea_agreement_number.$error ? false : null + }, + stateNordeaAgreementNumber () { + return (!this.$v.marketSettingsForm.nordea_agreement_number.$error) + }, + invalidNordeaAgreementNumber () { + if (this.$v.marketSettingsForm.nordea_agreement_number.$error) { + if (!this.$v.marketSettingsForm.nordea_agreement_number.numeric) + return this.$t('validation.numeric', { + field: this.$t('form.market.settings.nordea_agreement_number'), + }) + else if (!this.$v.marketSettingsForm.nordea_agreement_number.minLength) + return this.$t('validation.min_length', { + field: this.$t('form.market.settings.nordea_agreement_number'), + minLength: this.$v.marketSettingsForm.nordea_agreement_number.$params.minLength.min + }) + else if (!this.$v.marketSettingsForm.nordea_agreement_number.maxLength) + return this.$t('validation.max_length', { + field: this.$t('form.market.settings.nordea_agreement_number'), + maxLength: this.$v.marketSettingsForm.nordea_agreement_number.$params.maxLength.max + }) + } + return null + }, + stateNordeaSignerIDInput () { + return this.$v.marketSettingsForm.nordea_signer_id.$error ? false : null + }, + stateNordeaSignerID () { + return (!this.$v.marketSettingsForm.nordea_signer_id.$error) + }, + invalidNordeaSignerID () { + if (this.$v.marketSettingsForm.nordea_signer_id.$error) { + if (!this.$v.marketSettingsForm.nordea_signer_id.numeric) + return this.$t('validation.numeric', { + field: this.$t('form.market.settings.nordea_signer_id'), + }) + else if (!this.$v.marketSettingsForm.nordea_signer_id.minLength) + return this.$t('validation.min_length', { + field: this.$t('form.market.settings.nordea_signer_id'), + minLength: this.$v.marketSettingsForm.nordea_signer_id.$params.minLength.min + }) + else if (!this.$v.marketSettingsForm.nordea_signer_id.maxLength) + return this.$t('validation.max_length', { + field: this.$t('form.market.settings.nordea_signer_id'), + maxLength: this.$v.marketSettingsForm.nordea_signer_id.$params.maxLength.max + }) + } + return null + }, stateBankAccountInput () { return this.$v.marketSettingsForm.bank_account.$error ? false : null }, diff --git a/frontend/src-base/configs/index.js b/frontend/src-base/configs/index.js index 231562e6..dd7a7139 100644 --- a/frontend/src-base/configs/index.js +++ b/frontend/src-base/configs/index.js @@ -69,6 +69,10 @@ export const MIN_POSTAL_CODE_LENGTH = 3 export const MAX_POSTAL_CODE_LENGTH = 12 export const MAX_BANK_ACCOUNT_LENGTH = 10 export const BANK_REG_NUMBER_LENGTH = 4 +export const MIN_NORDEA_AGREEMENT_NUMBER_LENGTH = 10 +export const MAX_NORDEA_AGREEMENT_NUMBER_LENGTH = 18 +export const MIN_NORDEA_SIGNER_ID_LENGTH = 1 +export const MAX_NORDEA_SIGNER_ID_LENGTH = 13 export const MIN_SHELF_AMOUNT = 1 export const MAX_SHELF_AMOUNT = 1000 @@ -193,6 +197,7 @@ const SDC = 'sdc' const BEC = 'bec' const DANSKE_BANK = 'danske_bank' const NORDEA = 'nordea' +const NORDEA_ISO_20022 = 'nordea_iso_20022' const BANKDATA = 'bankdata' export const AVAILABLE_BANK_FORMATS = [ @@ -212,6 +217,10 @@ export const AVAILABLE_BANK_FORMATS = [ code: NORDEA, name: i18n.t(`multiselect.single_label.bank_export.${NORDEA}`), }, + { + code: NORDEA_ISO_20022, + name: i18n.t(`multiselect.single_label.bank_export.${NORDEA_ISO_20022}`), + }, { code: BANKDATA, name: i18n.t(`multiselect.single_label.bank_export.${BANKDATA}`), diff --git a/frontend/src-base/locale/da/da.json b/frontend/src-base/locale/da/da.json index 54cfce3e..03b4988b 100644 --- a/frontend/src-base/locale/da/da.json +++ b/frontend/src-base/locale/da/da.json @@ -120,8 +120,9 @@ }, "bank_export": { "missing_bank_data": "Udfyld butikkens reg. og konto nr. i {link}.", + "missing_nordea_data": "Indsæt aftalenummer og Signer ID in {link}.", "missing_bank_format": "Udfyld butikken bank fil format i {link}.", - "filename": "{filetype} {description}.txt", + "filename": "{filetype} {description}.{extension}", "back_button": "Tilbage" }, "economic_settings": { @@ -184,6 +185,7 @@ "bec": "BEC", "danske_bank": "Danske Bank", "nordea": "Nordea", + "nordea_iso_20022": "Nordea ISO 20022", "bankdata": "Bankdata" } } @@ -532,6 +534,8 @@ "support_email": "E-mail", "bank_reg_number": "Bank reg. nr.", "bank_account": "Bank konto nr.", + "nordea_agreement_number": "Nordea aftalenummer", + "nordea_signer_id": "Nordea Signer ID", "bank_file_format": "Bank fil format", "instagram_url": "Instagram link", "facebook_url": "Facebook link", diff --git a/frontend/src-base/locale/en/en.json b/frontend/src-base/locale/en/en.json index 23fce949..75bb8ac5 100644 --- a/frontend/src-base/locale/en/en.json +++ b/frontend/src-base/locale/en/en.json @@ -120,8 +120,9 @@ }, "bank_export": { "missing_bank_data": "Please specify your reg number and account number in {link}.", + "missing_nordea_data": "Please specify your Nordea Agreement no. and Signer ID in {link}.", "missing_bank_format": "Please specify your bank format in {link}.", - "filename": "{filetype} {description}.txt", + "filename": "{filetype} {description}.{extension}", "back_button": "Back" }, "economic_settings": { @@ -184,6 +185,7 @@ "bec": "BEC", "danske_bank": "Danske Bank", "nordea": "Nordea", + "nordea_iso_20022": "Nordea ISO 20022", "bankdata": "Bankdata" } } @@ -536,6 +538,8 @@ "support_email": "Email", "bank_reg_number": "Bank reg number", "bank_account": "Bank account", + "nordea_agreement_number": "Nordea Agreement no.", + "nordea_signer_id": "Nordea Signer ID", "bank_file_format": "Bank file format", "instagram_url": "Instagram URL", "facebook_url": "Facebook URL", diff --git a/loppeonline/api/v1/admin/api.py b/loppeonline/api/v1/admin/api.py index 201709e8..2d6e4a7f 100644 --- a/loppeonline/api/v1/admin/api.py +++ b/loppeonline/api/v1/admin/api.py @@ -542,7 +542,10 @@ def bank_export(self, request, pk=None): ) raise RestValidationError({'error': message}) - return Response({'file': b64encode(data)}) + return Response({ + 'file': b64encode(data), + 'extension': exporter.extension, + }) class AdminBankExportTransactionViewSet( diff --git a/loppeonline/api/v1/admin/serializers.py b/loppeonline/api/v1/admin/serializers.py index d95c12d0..b40ee1ea 100644 --- a/loppeonline/api/v1/admin/serializers.py +++ b/loppeonline/api/v1/admin/serializers.py @@ -135,6 +135,26 @@ class AdminMarketModelSerializer(serializers.ModelSerializer): required=False, allow_blank=True, ) + nordea_agreement_number = serializers.RegexField( + regex=r'^[0-9]{10,18}$', + error_messages={ + 'invalid': _( + 'This field must be numeric and contain 10 to 18 digits.' + ) + }, + required=False, + allow_blank=True, + ) + nordea_signer_id = serializers.RegexField( + regex=r'^[0-9]{1,13}$', + error_messages={ + 'invalid': _( + 'This field must be numeric and contain max. 13 digits.' + ) + }, + required=False, + allow_blank=True, + ) bank_file_format = fields.ChoiceField( choices=Market.BANK_FILE_FORMAT_CHOICES, allow_blank=True, @@ -300,6 +320,8 @@ class Meta: 'support_phone', 'bank_reg_number', 'bank_account', + 'nordea_agreement_number', + 'nordea_signer_id', 'bank_file_format', 'instagram_url', 'facebook_url', diff --git a/loppeonline/apps/markets/migrations/0020_auto_20201223_0031.py b/loppeonline/apps/markets/migrations/0020_auto_20201223_0031.py new file mode 100644 index 00000000..ae984d27 --- /dev/null +++ b/loppeonline/apps/markets/migrations/0020_auto_20201223_0031.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1.1 on 2020-12-22 23:31 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('markets', '0019_auto_20201211_1515'), + ] + + operations = [ + migrations.AddField( + model_name='market', + name='nordea_agreement_number', + field=models.CharField(blank=True, max_length=4, validators=[django.core.validators.RegexValidator('^[0-9]{10,18}$', 'Must contain 10 to 18 digits.')], verbose_name='Nordea Agreement no.'), + ), + migrations.AddField( + model_name='market', + name='nordea_signer_id', + field=models.CharField(blank=True, max_length=10, validators=[django.core.validators.RegexValidator('^[0-9]{1,13}$', 'Must contain max. 13 digits.')], verbose_name='Nordea Signer ID'), + ), + migrations.AlterField( + model_name='market', + name='bank_file_format', + field=models.CharField(blank=True, choices=[('sdc', 'SDC'), ('bec', 'BEC'), ('danske_bank', 'Danske Bank'), ('nordea', 'Nordea'), ('nordea_iso_20022', 'Nordea ISO 20022'), ('bankdata', 'Bankdata')], max_length=100), + ), + ] diff --git a/loppeonline/apps/markets/models.py b/loppeonline/apps/markets/models.py index 5c09d5ca..9177d9ff 100644 --- a/loppeonline/apps/markets/models.py +++ b/loppeonline/apps/markets/models.py @@ -27,6 +27,7 @@ class Market(models.Model): ('bec', _('BEC')), ('danske_bank', _('Danske Bank')), ('nordea', _('Nordea')), + ('nordea_iso_20022', _('Nordea ISO 20022')), ('bankdata', _('Bankdata')), ) @@ -66,6 +67,26 @@ class Market(models.Model): RegexValidator(r'^[0-9]{10}$', _("Must contain 10 digits.")) ], ) + nordea_agreement_number = models.CharField( + _('Nordea Agreement no.'), + max_length=4, + blank=True, + validators=[ + RegexValidator( + r'^[0-9]{10,18}$', _('Must contain 10 to 18 digits.') + ) + ], + ) + nordea_signer_id = models.CharField( + _('Nordea Signer ID'), + max_length=10, + blank=True, + validators=[ + RegexValidator( + r'^[0-9]{1,13}$', _('Must contain max. 13 digits.') + ) + ], + ) bank_file_format = models.CharField( max_length=100, choices=BANK_FILE_FORMAT_CHOICES, blank=True, ) diff --git a/loppeonline/apps/sales/bank_export/__init__.py b/loppeonline/apps/sales/bank_export/__init__.py index 5cd98931..d218a36d 100644 --- a/loppeonline/apps/sales/bank_export/__init__.py +++ b/loppeonline/apps/sales/bank_export/__init__.py @@ -2,6 +2,7 @@ from .bec import BECExporter from .danske_bank import DanskeBankExporter from .nordea import NordeaExporter +from .nordea_iso_20022 import NordeaISO20022Exporter from .sdc import SDCExporter @@ -10,5 +11,6 @@ 'bec': BECExporter, 'danske_bank': DanskeBankExporter, 'nordea': NordeaExporter, + 'nordea_iso_20022': NordeaISO20022Exporter, 'sdc': SDCExporter, } diff --git a/loppeonline/apps/sales/bank_export/bankdata.py b/loppeonline/apps/sales/bank_export/bankdata.py index e2ae49bc..648236eb 100644 --- a/loppeonline/apps/sales/bank_export/bankdata.py +++ b/loppeonline/apps/sales/bank_export/bankdata.py @@ -9,6 +9,8 @@ class BankdataExporter(BaseExporter): + extension = 'txt' + def format_start(self): values = [ 'IB000000000000', diff --git a/loppeonline/apps/sales/bank_export/bec.py b/loppeonline/apps/sales/bank_export/bec.py index a8d863d8..32cdc6a1 100644 --- a/loppeonline/apps/sales/bank_export/bec.py +++ b/loppeonline/apps/sales/bank_export/bec.py @@ -13,6 +13,8 @@ class BECExporter(BaseExporter): + extension = 'txt' + def format_transaction(self, transaction): market_reg_number = self.market.bank_reg_number market_account_number = self.market.bank_account diff --git a/loppeonline/apps/sales/bank_export/danske_bank.py b/loppeonline/apps/sales/bank_export/danske_bank.py index e5e50e0b..c4921175 100644 --- a/loppeonline/apps/sales/bank_export/danske_bank.py +++ b/loppeonline/apps/sales/bank_export/danske_bank.py @@ -13,6 +13,8 @@ class DanskeBankExporter(BaseExporter): + extension = 'txt' + def format_transaction(self, transaction): market_reg_number = self.market.bank_reg_number market_account_number = self.market.bank_account diff --git a/loppeonline/apps/sales/bank_export/nordea.py b/loppeonline/apps/sales/bank_export/nordea.py index d77ef06b..766263de 100644 --- a/loppeonline/apps/sales/bank_export/nordea.py +++ b/loppeonline/apps/sales/bank_export/nordea.py @@ -11,6 +11,8 @@ class NordeaExporter(BaseExporter): + extension = 'txt' + def format_transaction(self, transaction): customer_name = transaction.customer.first_name customer_reg_number = transaction.customer.customer.bank_reg_number diff --git a/loppeonline/apps/sales/bank_export/nordea_iso_20022.py b/loppeonline/apps/sales/bank_export/nordea_iso_20022.py new file mode 100644 index 00000000..60e9443a --- /dev/null +++ b/loppeonline/apps/sales/bank_export/nordea_iso_20022.py @@ -0,0 +1,43 @@ +from io import TextIOWrapper + +from django.template.loader import render_to_string + +from .base import BaseExporter +from .exceptions import BankExportException + + +class NordeaISO20022Exporter(BaseExporter): + extension = 'xml' + + def validate_market(self): + missing_data = '' in ( + self.market.bank_reg_number, + self.market.bank_account, + self.market.nordea_agreement_number, + self.market.nordea_signer_id, + ) + + if missing_data: + raise BankExportException + + def render(self): + transactions = self.get_transactions() + + return render_to_string( + 'bank_export/nordea_iso_20022.xml', + { + 'payment_date': self.payment_date, + 'market': self.market, + 'pisp': self.pisp, + 'transactions': transactions, + }, + ) + + def write_data(self): + self.validate_market() + + wrapper = TextIOWrapper(self.output) + + wrapper.write(self.render()) + + wrapper.detach() diff --git a/loppeonline/apps/sales/bank_export/sdc.py b/loppeonline/apps/sales/bank_export/sdc.py index 806d4d2a..d90f1af6 100644 --- a/loppeonline/apps/sales/bank_export/sdc.py +++ b/loppeonline/apps/sales/bank_export/sdc.py @@ -8,6 +8,8 @@ class SDCExporter(BaseExporter): + extension = 'txt' + def format_transaction(self, transaction): market_reg_number = self.market.bank_reg_number market_account_number = self.market.bank_account diff --git a/loppeonline/apps/sales/templatetags/bank_export.py b/loppeonline/apps/sales/templatetags/bank_export.py new file mode 100644 index 00000000..a5b6ccb2 --- /dev/null +++ b/loppeonline/apps/sales/templatetags/bank_export.py @@ -0,0 +1,11 @@ +from django import template + +from loppeonline.apps.sales.bank_export import utils as bank_export_utils + + +register = template.Library() + + +@register.filter +def format_decimal(value, arg): + return bank_export_utils.format_decimal(value, sep=arg) diff --git a/loppeonline/templates/bank_export/nordea_iso_20022.xml b/loppeonline/templates/bank_export/nordea_iso_20022.xml new file mode 100644 index 00000000..e6ef686d --- /dev/null +++ b/loppeonline/templates/bank_export/nordea_iso_20022.xml @@ -0,0 +1,103 @@ +{% load bank_export %} + + + {{ pisp.id }} + {% now "Y-m-d\TH:i:s" %} + {{ transactions.count }} + + + + + {{ market.nordea_signer_id }} + + CUST + + + + + + + + {{ pisp.id }} + TRF + + NORM + + NURG + + + {{ payment_date|date:"Y-m-d" }} + + {{ market.visual_name|default:market.market_id }} + + DK + + + + + {{ market.nordea_agreement_number }} + + BANK + + + + + + + + + {{ market.bank_reg_number }}{{ market.bank_account }} + + BBAN + + + + DKK + + + + NDEADKKK + + DK + + + + {% for transaction in transactions %} + + + {{ transaction.id }} + + + {{ transaction.amount|format_decimal:"." }} + + + + NDEADKKK + + DK + + + + + {{ transaction.customer.first_name }} + + DK + + + + + + {{ transaction.customer.customer.bank_reg_number }}{{ transaction.customer.customer.bank_account }} + + BBAN + + + + + + Afr: {{ pisp.start_date|date:"Y-m-d" }} + + + {% endfor %} + + From 739fc8090d5a961f9e1eb9b5013cc401359af04f Mon Sep 17 00:00:00 2001 From: aruseni Date: Wed, 23 Dec 2020 14:27:06 +0200 Subject: [PATCH 2/2] Refactoring --- frontend/src-admin/views/BankExportView.vue | 8 +++++--- frontend/src-base/configs/index.js | 12 ++++++------ loppeonline/apps/sales/bank_export/bec.py | 1 - loppeonline/apps/sales/bank_export/danske_bank.py | 1 - loppeonline/apps/sales/bank_export/nordea.py | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend/src-admin/views/BankExportView.vue b/frontend/src-admin/views/BankExportView.vue index 45505670..9b7cda45 100644 --- a/frontend/src-admin/views/BankExportView.vue +++ b/frontend/src-admin/views/BankExportView.vue @@ -80,8 +80,9 @@