def generate_batch(self): sepa = SepaDD(settings.SEPA_CONFIG,schema="pain.008.001.02", clean=True) payments = [] for batch_result in SepaBatchResult.objects.filter(batch=self): payment = batch_result.payment if payment.amount <= 0: batch_result.success = False batch_result.fail_reason = ZERO_AMOUNT if not payment.account or not payment.account.iban_code: batch_result.success = False batch_result.fail_reason = IBAN_MISSING else: batch_result.iban_code = payment.account.iban_code[4:8] bank = BankBICCode.objects.filter(bank_code=batch_result.iban_code).first() if not bank: batch_result.success = False batch_result.fail_reason = BIC_MISSING if batch_result.success: batch_result.bic_code = bank.bic_code batch_result.bank_name = bank.bank_name pay = { "name": payment.account.display_name, "IBAN": payment.account.iban_code, "amount": int(payment.amount * 100), "BIC": batch_result.bic_code, "type": "RCUR", "collection_date": datetime.date.today(), "mandate_id": payment.account.cif, "execution_date": datetime.date.today(), "mandate_date": datetime.date.today(), "description": payment.concept, "endtoend_id": str(payment.reference).replace('-',''), } sepa.add_payment(pay) payments.append(batch_result.payment) batch_result.save() if (len(payments) > 0): sepa_xml = sepa.export(validate=True) xml_temp = NamedTemporaryFile() xml_temp.write(sepa_xml) xml_temp.flush() self.sepa_file.save(f"sepa_batch_{self.pk}.xml", File(xml_temp)) self.save() # We check the included payments as paid for payment in payments: payment.completed = True payment.timestamp = datetime.datetime.now() payment.type = DEBIT payment.save()
def __init__(self): self.invoices = [] self.config = { "name": app.config.get('SEPADD_CREDITOR_NAME'), "IBAN": app.config.get('SEPADD_CREDITOR_IBAN'), "BIC": app.config.get('SEPADD_CREDITOR_BIC'), "batch": app.config.get('SEPADD_BATCH'), "creditor_id": app.config.get('SEPADD_CREDITOR_ID'), "currency": app.config.get('SEPADD_CURRENCY'), "instrument": app.config.get('SEPADD_INSTRUMENT') } self.sepa = SepaDD(self.config, schema=app.config.get('SEPADD_SCHEMA'))
def test_name_too_long(): sdd = SepaDD({ "name": "TestCreditor", "BIC": "BANKNL2A", "IBAN": "NL50BANK1234567890", "batch": True, "creditor_id": "000000", "currency": "EUR" }) payment1 = { "name": "Test von Testenstein Test von Testenstein Test von Testenstein", "IBAN": "NL50BANK1234567890", "BIC": "BANKNL2A", "amount": 1012, "type": "FRST", "collection_date": datetime.date.today(), "mandate_id": "1234", "mandate_date": datetime.date.today(), "description": "Test transaction1" } sdd.add_payment(payment1) with pytest.raises(ValidationError): sdd.export() sdd.export(validate=False)
def generate_batch(self): sepa = SepaDD(settings.SEPA_CONFIG, schema="pain.008.001.02", clean=True) payments = [] for batch_result in SepaBatchResult.objects.filter(batch=self): payment = batch_result.payment if batch_result.success: pay = { "name": payment.account.display_name, "IBAN": payment.account.iban_code, "amount": int(payment.amount * 100), "BIC": batch_result.bic_code, "type": "RCUR", "collection_date": datetime.date.today(), "mandate_id": payment.account.cif, "execution_date": datetime.date.today(), "mandate_date": datetime.date.today(), "description": payment.concept, "endtoend_id": str(payment.reference).replace('-', ''), } sepa.add_payment(pay) payments.append(batch_result.payment) payment.invoice_prefix = self.invoice_prefix payment.invoice_number = batch_result.invoice_number payment.invoice_date = self.attempt payment.save() if (len(payments) > 0): sepa_xml = sepa.export(validate=True) xml_temp = NamedTemporaryFile() xml_temp.write(sepa_xml) xml_temp.flush() self.sepa_file.save(f"sepa_batch_{self.pk}.xml", File(xml_temp)) self.save() # We update the payments date for payment in payments: #payment.added = datetime.datetime.now() payment.type = DEBIT payment.save()
def test_valid_config(): return SepaDD({ "name": "TestCreditor", "IBAN": "NL50BANK1234567890", "BIC": "BANKNL2A", "batch": True, "creditor_id": "000000", "currency": "EUR" })
def test_invalid_config(): with pytest.raises(Exception): return SepaDD({ "name": "TestCreditor", "BIC": "BANKNL2A", "batch": True, "creditor_id": "000000", "currency": "EUR" })
def sdd(): return SepaDD( { "name": "TestCreditor", "IBAN": "NL50BANK1234567890", "BIC": "BANKNL2A", "batch": True, "creditor_id": "DE26ZZZ00000000000", "currency": "EUR" }, schema="pain.008.003.02")
def create_sepa_xml(input_file: IO[str], output_path: str, config: Dict[str, Any]) -> None: """Creates a XML file from CSV data. Parameters ---------- input_file: IO[str] Path to the CSV data. output_path: str Path to the output data. Default is in the project-root/output. config: Dict[str, Any] Config.json for SEPA XML generation. """ today_s = str(date.today()) hit_credit = False hit_debit = False sepa_credit = SepaTransfer(config, clean=True) sepa_debit = SepaDD(config, schema='pain.008.002.02', clean=True) echo('Getting Data...') for i, payment in enumerate(single_csv_row(input_file)): p, p_type = _pack_data(payment) if p_type == 'credit': hit_credit = True sepa_credit.add_payment(p) if p_type == 'debit': hit_debit = True sepa_debit.add_payment(p) if hit_credit: _generate_output(today_s + '_ueberweisung', sepa_credit.export(), output_path) if hit_debit: _generate_output(today_s + '_gutschrift', sepa_debit.export(), output_path)
def post(self, request, *args, **kwargs): self._event_cache = {} valid_payments = defaultdict(list) files = {} for payment in self.get_unexported().select_related( 'order', 'order__event', 'sepadebit_due'): if not payment.info_data: # Should not happen # TODO: Notify user payment.state = OrderPayment.PAYMENT_STATE_FAILED payment.save() payment.order.status = Order.STATUS_PENDING payment.order.save() continue payment_dict = { "name": payment.info_data['account'], "IBAN": payment.info_data['iban'], "BIC": payment.info_data['bic'], "amount": int(payment.amount * 100), "type": "OOFF", "collection_date": max(now().astimezone(payment.order.event.timezone).date(), payment.sepadebit_due.date), "mandate_id": payment.info_data['reference'], "mandate_date": (payment.order.datetime if payment.migrated else payment.created).date(), "description": _('Event ticket {event}-{code}').format( event=payment.order.event.slug.upper(), code=payment.order.code) } config = self._config_for_event(payment.order.event) if config not in files: files[config] = SepaDD(dict(config), schema='pain.008.001.02') file = files[config] file.add_payment(payment_dict) valid_payments[file].append(payment) if valid_payments: with transaction.atomic(): for k, f in list(files.items()): if hasattr(request, 'event'): exp = SepaExport(event=request.event, xmldata='') exp.testmode = request.event.testmode else: exp = SepaExport(organizer=request.organizer, xmldata='') exp.testmode = False exp.xmldata = f.export(validate=False).decode('utf-8') import xmlschema # xmlschema does some weird monkeypatching in etree, if we import it globally, things fail my_schema = xmlschema.XMLSchema( os.path.join(os.path.dirname(validation.__file__), 'schemas', f.schema + '.xsd')) errs = [] for e in my_schema.iter_errors(exp.xmldata): errs.append(str(e)) if errs: messages.error( request, _('The generated file did not validate for the following reasons. ' 'Please contact pretix support for more information.\n{}' ).format("\n".join(errs))) del files[k] else: exp.currency = f._config['currency'] exp.save() SepaExportOrder.objects.bulk_create([ SepaExportOrder(order=p.order, payment=p, export=exp, amount=p.amount) for p in valid_payments[f] ]) if len(files) > 1: messages.warning( request, _('Multiple new export files have been created, since your events ' 'have differing SEPA settings. Please make sure to process all of them!' )) elif len(files) > 0: messages.success(request, _('A new export file has been created.')) else: messages.warning(request, _('No valid orders have been found.')) if hasattr(request, 'event'): return redirect( reverse('plugins:pretix_sepadebit:export', kwargs={ 'event': request.event.slug, 'organizer': request.organizer.slug, })) else: return redirect( reverse('plugins:pretix_sepadebit:export', kwargs={ 'organizer': request.organizer.slug, }))
def form_valid(self, form): config = DirectDebitConfiguration.get_solo() global_config = Configuration.get_solo() members = self._get_members() now_ = now() dd_config = { "name": form.cleaned_data['own_name'], "IBAN": str(form.cleaned_data['own_iban']), "BIC": str(form.cleaned_data['own_bic']), "batch": True, "creditor_id": config.creditor_id, "currency": global_config.currency, "instrument": "COR1" if form.cleaned_data['cor1'] else "CORE", } sepa = SepaDD(dd_config, schema=form.cleaned_data['sepa_format'], clean=True) exp_member_numbers = [ ((int(x.split("-")[0]), int(x.split("-")[1])) if "-" in x else (int(x), int(x))) for x in form.cleaned_data['exp_member_numbers'].split(",") ] with atomic(): debit = DirectDebit( datetime=now_, multiple=True, cor1=form.cleaned_data['cor1'], pain_descriptor='urn:iso:std:iso:20022:tech:xsd:' + form.cleaned_data['sepa_format'], additional_data={ 'login_pk': self.selected_account_login_pk, 'account_iban': form.cleaned_data['own_iban'], 'account_bic': form.cleaned_data['own_bic'], }) debit_payments = [] for member in members: ## Experimental gates if form.cleaned_data['exp_bank_types'] == "DE": if not member.profile_sepa.iban.upper().startswith("DE"): continue elif form.cleaned_data['exp_bank_types'] == "NDE": if member.profile_sepa.iban.upper().startswith("DE"): continue if exp_member_numbers: if not any(a <= int(member.number) <= b for (a, b) in exp_member_numbers): continue debit_payment = DirectDebitPayment( id=uuid4(), type='FRST', ## FIXME Based on existing data mandate_reference=member.profile_sepa.mandate_reference, collection_date=form.cleaned_data['debit_date'], amount=-member.balance, direct_debit=debit, member=member, ) payment = { "name": member.name, "IBAN": member.profile_sepa.iban, "BIC": member.profile_sepa.bic_autocomplete, "collection_date": debit_payment.collection_date, "amount": int(debit_payment.amount * 100), # in cents "type": debit_payment.type, "mandate_id": debit_payment.mandate_reference, "mandate_date": member.profile_sepa.issue_date or now_.date(), "description": form.cleaned_data['debit_text'], "endtoend_id": debit_payment.id.hex, } sepa.add_payment(payment) debit_payments.append(debit_payment) context = { 'creditor_id': config.creditor_id, 'sepa_mandate_reference': debit_payment.mandate_reference, 'sepa_iban': member.profile_sepa.iban, 'sepa_bic': member.profile_sepa.bic_autocomplete, 'contact': global_config.mail_from, 'association_name': global_config.name, 'additional_information': '', 'debit_date': form.cleaned_data['debit_date'], 'amount': "%.2f %s" % (debit_payment.amount, global_config.currency), } mail = config.debit_notification_template.to_mail( member.email, context=context, save=False, ) mail.text = form.cleaned_data['text'].format(**context) mail.subject = form.cleaned_data['subject'].format(**context) mail.save() mail.members.add(member) debit.sepa_xml = sepa.export(validate=True).decode('utf-8') debit.save() for p in debit_payments: p.save() return HttpResponseRedirect( reverse('plugins:byro_directdebit:finance.directdebit.transmit_dd', kwargs={'pk': debit.pk}))
class SepaExport: def __init__(self): self.invoices = [] self.config = { "name": app.config.get('SEPADD_CREDITOR_NAME'), "IBAN": app.config.get('SEPADD_CREDITOR_IBAN'), "BIC": app.config.get('SEPADD_CREDITOR_BIC'), "batch": app.config.get('SEPADD_BATCH'), "creditor_id": app.config.get('SEPADD_CREDITOR_ID'), "currency": app.config.get('SEPADD_CURRENCY'), "instrument": app.config.get('SEPADD_INSTRUMENT') } self.sepa = SepaDD(self.config, schema=app.config.get('SEPADD_SCHEMA')) def __len__(self): return len(self.invoices) def add_invoice(self, invoice): if (len(invoice.items) == 0): # skip invoices without items return if not invoice.sent: raise Exception("Invoice %s has not yet been sent." % invoice.number) if invoice.cancelled: raise Exception("Invoice %s is cancelled." % invoice.number) if not invoice.contact.has_sepa_mandate: raise Exception("Invoice %s: No sepa mandate for %s" % (invoice.number, invoice.contact)) if invoice.payment_type != 'SEPA-DD': raise Exception("Invoice %s: Payment Type is not SEPA-DD" % invoice.number) if self.sepa.check_payment(self._gen_payment(invoice)): self.invoices.append(invoice) def _gen_payment(self, invoice): if invoice.contact.sepa_mandate_first: payment_type = "FRST" collection_date = date.today() + timedelta(days=+5) else: payment_type = "RCUR" collection_date = date.today() + timedelta(days=+3) payment = { "name": invoice.contact.name, "IBAN": invoice.contact.sepa_iban, "mandate_id": invoice.contact.sepa_mandate_id, "mandate_date": invoice.contact.sepa_mandate_date, "amount": int(invoice.amount * 100), "type": payment_type, # FRST,RCUR,OOFF,FNAL "collection_date": collection_date, "endtoend_id": invoice.exported_id, "description": "Funkfeuer %s%d %s" % (app.config.get('BILLING_REFERENCE_UID_PREFIX'), invoice.contact.id, invoice.number) } return payment def add_invoices(self, invoices): for invoice in invoices: self.add_invoice(invoice) @property def msg_id(self): return self.sepa.msg_id def export(self): if len(self.invoices) < 1: raise Exception("no invoices to export") for invoice in self.invoices: if invoice.exported_id is None: invoice.exported_id = make_id( invoice.number, app.config.get('SEPADD_CREDITOR_NAME')) invoice.exported = True self.sepa.add_payment(self._gen_payment(invoice)) if invoice.contact.sepa_mandate_first: invoice.contact.sepa_mandate_first = False export = self.sepa.export() db.session.add( model.Job(type='sepa_export', note=self.msg_id, user=current_user, started=datetime.utcnow(), finished=datetime.utcnow())) db.session.commit() return export
def generate_xml_file(self): from sepaxml import SepaDD sepa_settings = frappe.get_doc("Sepa Direct Debit Settings", self.company) company_iban, company_bic = frappe.db.get_value( "Bank Account", sepa_settings.bank_account, ["iban", "swift_number"]) config = { "name": sepa_settings.company_name, "IBAN": company_iban, "BIC": company_bic, "batch": self.batch_booking, "creditor_id": sepa_settings. creditor_identifier, # supplied by your bank or financial authority "currency": self.currency, # ISO 4217 "instrument": sepa_settings.instrument # - default is CORE (B2C) } sepa = SepaDD(config, schema=sepa_settings.schema or "pain.008.001.02", clean=True) for payment_entry in self.payment_entries: payment_types = { "One-off": "OOFF", "First": "FRST", "Recurrent": "RCUR", "Final": "FNAL" } payment_type = self.direct_debit_type customer = payment_entry.against_account if not frappe.db.exists( "Sepa Mandate", dict(customer=customer, registered_on_gocardless=0, status="Active")): frappe.throw( _("Please create or activate a SEPA Mandate for customer {0}" .format(customer))) else: mandate = frappe.get_doc("Sepa Mandate", dict(customer=customer)) customer_iban, customer_bic = frappe.db.get_value( "Bank Account", mandate.bank_account, ["iban", "swift_number"]) pe = frappe.get_doc("Payment Entry", payment_entry.payment_entry) sales_invoices = "" for ref in pe.references: sales_invoices += "/" + ref.reference_name payment_amount = cint(payment_entry.amount * 100) payment = { "name": customer, "IBAN": customer_iban, "BIC": customer_bic, "amount": payment_amount, # in cents "type": payment_types.get(payment_type), # FRST,RCUR,OOFF,FNAL "collection_date": getdate(payment_entry.reference_date), "mandate_id": mandate.mandate, "mandate_date": mandate.creation_date, "description": sepa_settings.reference_prefix + sales_invoices, "endtoend_id": pe.reference_no # autogenerated if obmitted } sepa.add_payment(payment) try: sepa_export = sepa.export( validate=False) # TODO: correct false positive upon validation except Exception as e: frappe.throw(str(e)) self.save_sepa_export(sepa_export) return sepa_export