Make cmutuel system use the Generic Managed Bank Transaction interface
authorMagnus Hagander <magnus@hagander.net>
Sat, 9 Feb 2019 21:44:06 +0000 (22:44 +0100)
committerMagnus Hagander <magnus@hagander.net>
Sat, 9 Feb 2019 21:50:34 +0000 (22:50 +0100)
This makes it integrate nicer in the system, including invoice
processing and payout matching.

code/pgeusite/cmutuel/apps.py
code/pgeusite/cmutuel/management/commands/cmscrape.py
code/pgeusite/cmutuel/util.py [new file with mode: 0644]
code/skin_settings.py
template/cmutuel/payment.html [new file with mode: 0644]

index 55f3e886e70c58fdfc902dd7791642ebad747f23..c8fac75435bfae926688a5a61a32c896b3930d9a 100644 (file)
@@ -1,5 +1,12 @@
 from django.apps import AppConfig
 
+
 class cmConfig(AppConfig):
     name = 'pgeusite.cmutuel'
     verbose_name = 'Credit Mutuel'
+
+    def ready(self):
+        # Must be imported here since the class is loaded too early
+        from postgresqleu.util.payment import register_payment_implementation
+
+        register_payment_implementation('pgeusite.cmutuel.util.CMutuelPayment')
index 11f862ffa337677c26ac916f962a21e9946afe78..fe4cf8611ad2110242e0586a6c8f790d48509b36 100755 (executable)
@@ -1,7 +1,7 @@
 # Scrape the CM pages to fetch list of transactions
 #
 #
-# Copyright (C) 2014, PostgreSQL Europe
+# Copyright (C) 2014-2019, PostgreSQL Europe
 #
 from django.core.management.base import BaseCommand, CommandError
 from django.db import transaction
@@ -19,6 +19,8 @@ from html.parser import HTMLParser
 
 
 from postgresqleu.mailqueue.util import send_simple_mail
+from postgresqleu.invoices.util import register_bank_transaction
+from postgresqleu.invoices.models import InvoicePaymentMethod
 
 from pgeusite.cmutuel.models import CMutuelTransaction
 
@@ -73,17 +75,14 @@ class Command(BaseCommand):
 
         @classmethod
         def should_run(self):
-            if not settings.CM_USER_ACCOUNT:
-                return False
-            return True
-
+            return InvoicePaymentMethod.objects.filter(active=True, classname='pgeusite.cmutuel.util.CMutuelPayment').exists()
 
     def add_arguments(self, parser):
         parser.add_argument('-q', '--quiet', action='store_true')
 
     def handle(self, *args, **options):
-        if not settings.CM_USER_ACCOUNT:
-            raise CommandError("Must specify CM user account in local_settings.py!")
+        method = InvoicePaymentMethod.objects.get(active=True, classname='pgeusite.cmutuel.util.CMutuelPayment')
+        pm = method.get_implementation()
 
         verbose = not options['quiet']
 
@@ -94,8 +93,8 @@ class Command(BaseCommand):
 
         sess.expect_redirect('https://www.creditmutuel.fr/en/authentification.html',
                              'https://www.creditmutuel.fr/en/banque/pageaccueil.html', {
-                                 '_cm_user': settings.CM_USER_ACCOUNT,
-                                 '_cm_pwd': settings.CM_USER_PASSWORD,
+                                 '_cm_user': pm.config('user'),
+                                 '_cm_pwd': pm.config('password'),
                                  'flag': 'password',
                              })
 
@@ -185,35 +184,40 @@ class Command(BaseCommand):
                     balance = Decimal(row[4])
 
                     if not CMutuelTransaction.objects.filter(opdate=opdate, valdate=valdate, amount=amount, description=description).exists():
-                        CMutuelTransaction(opdate=opdate,
-                                           valdate=valdate,
-                                           amount=amount,
-                                           description=description,
-                                           balance=balance).save()
+                        trans = CMutuelTransaction(opdate=opdate,
+                                                   valdate=valdate,
+                                                   amount=amount,
+                                                   description=description,
+                                                   balance=balance)
+                        trans.save()
+
+                        # Also send the transaction into the main system. Unfortunately we don't
+                        # know the sender.
+                        # register_bank_transaction returns True if the transaction has been fully
+                        # processed and thus don't need anything else, so we just consider it
+                        # sent already.
+                        if register_bank_transaction(method, trans.id, amount, description, ''):
+                            trans.sent = True
+                            trans.save()
                 except Exception as e:
                     sys.stderr.write("Exception '{0}' when parsing row {1}".format(e, row))
 
         # Now send things off if there is anything to send
         with transaction.atomic():
-            if CMutuelTransaction.objects.filter(sent=False).exclude(
-                    Q(description__startswith='VIR STG ADYEN ') |
-                    Q(description__startswith='VIR ADYEN BV ') |
-                    Q(description__startswith='VIR ADYEN NV ')
-            ).exists():
+            if CMutuelTransaction.objects.filter(sent=False).exists():
                 sio = io.StringIO()
                 sio.write("One or more new transactions have been recorded in the Credit Mutuel account:\n\n")
                 sio.write("%-10s  %15s  %s\n" % ('Date', 'Amount', 'Description'))
                 sio.write("-" * 50)
                 sio.write("\n")
-                for cmt in CMutuelTransaction.objects.filter(sent=False).order_by('opdate'):
-                    # Maybe this shouldn't be hardcoded, but for now it is.
-                    # Exclude Adyen transactions, since they are already reported separately.
-                    # Still flag them as sent though, so they don't queue up forever.
-                    if not (cmt.description.startswith('VIR STG ADYEN ') or cmt.description.startswith('VIR ADYEN BV ') or cmt.description.startswith('VIR ADYEN NV ')):
-                        sio.write("%10s  %15s  %s\n" % (cmt.opdate, cmt.amount, cmt.description))
 
+                for cmt in CMutuelTransaction.objects.filter(sent=False).order_by('opdate'):
+                    sio.write("%10s  %15s  %s\n" % (cmt.opdate, cmt.amount, cmt.description))
                     cmt.sent = True
                     cmt.save()
+
+                sio.write("\n\nYou will want to go processes these at:\n{0}/admin/invoices/banktransactions/".format(settings.SITEBASE))
+
                 send_simple_mail(settings.INVOICE_SENDER_EMAIL,
                                  settings.INVOICE_SENDER_EMAIL,
                                  'New Credit Mutuel transactions',
diff --git a/code/pgeusite/cmutuel/util.py b/code/pgeusite/cmutuel/util.py
new file mode 100644 (file)
index 0000000..54f43da
--- /dev/null
@@ -0,0 +1,39 @@
+from django import forms
+from django.shortcuts import render
+from django.template import Template, Context
+from django.db.models import Sum
+
+from urllib.parse import urlencode
+
+from postgresqleu.util.payment.banktransfer import BaseManagedBankPayment
+from postgresqleu.util.payment.banktransfer import BaseManagedBankPaymentForm
+from postgresqleu.invoices.models import Invoice, BankTransferFees
+
+
+class BackendCMutuelForm(BaseManagedBankPaymentForm):
+    user = forms.CharField(required=True, label="User account", help_text="Username used to log in")
+    password = forms.CharField(required=True, widget=forms.widgets.PasswordInput(render_value=True))
+
+    managed_fields = ['user', 'password', ]
+    managed_fieldsets = [
+        {
+            'id': 'cm',
+            'legend': 'Credit Mutuel',
+            'fields': ['user', 'password', ],
+        }
+    ]
+
+
+class CMutuelPayment(BaseManagedBankPayment):
+    backend_form_class = BackendCMutuelForm
+    description = """
+Pay using a direct IBAN bank transfer in EUR. We
+<strong>strongly advice</strong> not using this method if
+making a payment from outside the Euro-zone, as amounts
+must be exact and all fees covered by sender.
+"""
+
+    def render_page(self, request, invoice):
+        return render(request, 'cmutuel/payment.html', {
+            'invoice': invoice,
+        })
index 0a2cc1416ea47bfc091bb8e9da6a2f8d63d2ecb9..e7ee11af4558350f34a3ebf2da126999e3b31841 100644 (file)
@@ -29,7 +29,3 @@ EU_VAT_VALIDATE = True
 # Invoice
 INVOICE_PDF_BUILDER = 'pgeuinvoices.PGEUInvoice'
 REFUND_PDF_BUILDER = 'pgeuinvoices.PGEURefund'
-
-# CM balance fetching account
-CM_USER_ACCOUNT = None
-CM_USER_PASSWORD = None
diff --git a/template/cmutuel/payment.html b/template/cmutuel/payment.html
new file mode 100644 (file)
index 0000000..5385e50
--- /dev/null
@@ -0,0 +1,52 @@
+{%extends "navbase.html"%}
+{%block title%}Pay with bank transfer{%endblock%}
+{%block content%}
+<h1>Pay with bank transfer</h1>
+<p>
+To pay your invoice using bank transfer, please make a payment
+according to the following:
+</p>
+
+<table border="1" cellspacing="0" cellpadding="3">
+<tr>
+ <th>Account holder</th>
+ <td>Association PostgreSQL Europe</td>
+</tr>
+<tr>
+ <th>Bank name</th>
+ <td>CCM PARIS 1-2 LOUVRE MONTORGUEIL</td>
+</tr>
+<tr>
+ <th>BIC</th>
+ <td>CMCIFR2A</td>
+</tr>
+<tr>
+ <th>IBAN</th>
+ <td>FR76 1027 8060 3100 0205 2290 114</td>
+</tr>
+<tr>
+ <th>Payment reference</th>
+ <td>{{invoice.payment_reference}}</td>
+</tr>
+<tr>
+ <th>Amount</th>
+ <td>€{{invoice.total_amount}}</td>
+</tr>
+</table>
+
+<p>
+<b>Note</b> that it is <b><i>very</i></b> important that you provide the
+correct text on the transfer, or we may not be able to match your payment
+to the correct invoice. In particular, <b>do not</b> use the invoice number,
+use the given payment reference!
+</p>
+
+<p>
+<b>Note</b> that bank transfers take a few days to process, so if your
+payment is nedeed repidly in order to confirm something, this is not a good
+choice of payment method.
+</p>
+
+{%if returnurl%}<a href="{{returnurl}}" class="btn btn-outline-dark">Return to payment options</a>{%endif%}
+
+{%endblock%}