diff --git a/pyproject.toml b/pyproject.toml index 2412ca9ac6..852c2124e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,3 +68,4 @@ max-complexity = 16 [tool.ruff.lint.per-file-ignores] "payments/backends.py" = ["T201"] "scripts/*" = ["T201"] +"weblate_web/migrations/0031_fill_in_customer.py" = ["T201"] diff --git a/weblate_web/admin.py b/weblate_web/admin.py index 740adeea33..b3b93be129 100644 --- a/weblate_web/admin.py +++ b/weblate_web/admin.py @@ -50,7 +50,7 @@ class DonationAdmin(admin.ModelAdmin): "reward", "active", ] - autocomplete_fields = ("user",) + autocomplete_fields = ("user", "customer") class ProjectAdmin(admin.TabularInline): @@ -82,7 +82,7 @@ class ServiceAdmin(admin.ModelAdmin): "note", ) date_hierarchy = "created" - autocomplete_fields = ("users",) + autocomplete_fields = ("users", "customer") inlines = (ProjectAdmin,) def get_form(self, request, obj=None, **kwargs): diff --git a/weblate_web/migrations/0030_donation_customer_service_customer.py b/weblate_web/migrations/0030_donation_customer_service_customer.py new file mode 100644 index 0000000000..164e8ed436 --- /dev/null +++ b/weblate_web/migrations/0030_donation_customer_service_customer.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.6 on 2024-10-11 11:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("payments", "0028_alter_customer_email"), + ("weblate_web", "0029_package_category"), + ] + + operations = [ + migrations.AddField( + model_name="donation", + name="customer", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="payments.customer", + ), + ), + migrations.AddField( + model_name="service", + name="customer", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="payments.customer", + ), + ), + ] diff --git a/weblate_web/migrations/0031_fill_in_customer.py b/weblate_web/migrations/0031_fill_in_customer.py new file mode 100644 index 0000000000..722fd26443 --- /dev/null +++ b/weblate_web/migrations/0031_fill_in_customer.py @@ -0,0 +1,88 @@ +# Generated by Django 5.0.6 on 2024-10-11 11:59 + +from django.conf import settings +from django.db import migrations +from django_countries import countries +from fakturace.storage import InvoiceStorage + + +def get_customer(customer_model, payments, invoice_storage, instance_name): + # First look at the invoice + for payment in payments: + if not payment.invoice: + continue + try: + invoice = invoice_storage.get(payment.invoice) + except KeyError: + print(f"<{instance_name}>: Could not load invoice {payment.invoice}!") + continue + + # Try using database model + contact_id = invoice.invoice["contact"] + if contact_id.startswith("web-"): + contact_pk = int(contact_id[4:]) + try: + return customer_model.objects.get(pk=contact_pk) + except customer_model.DoesNotExist: + print( + f"<{instance_name}>: Could not load contact {contact_pk} ({contact_id})" + ) + + # Create from the data + print(f"<{instance_name}>: Matching customer for {invoice.contact['name']}") + return customer_model.objects.get_or_create( + vat=invoice.contact.get("vat_reg", None), + name=invoice.contact["name"], + address=invoice.contact["address"], + city=invoice.contact["city"], + country=countries.by_name(invoice.contact["country"]), + user_id=-1, + origin="https://weblate.org/auto", + email=invoice.contact.get("email", ""), + )[0] + + print(f"<{instance_name}>: Fallback to model: {payments[0].customer.name}") + + # Fallback to model + return payments[0].customer + + +def update_customer(apps, schema_editor): + print() + Donation = apps.get_model("weblate_web", "Donation") + Service = apps.get_model("weblate_web", "Service") + Payment = apps.get_model("payments", "Payment") + Customer = apps.get_model("payments", "Customer") + + invoice_storage = InvoiceStorage(settings.PAYMENT_FAKTURACE) + + for donation in Donation.objects.filter(customer=None): + payment = Payment.objects.get(pk=donation.payment) + donation.customer = get_customer( + Customer, [payment], invoice_storage, f"Donation {donation.id}" + ) + donation.save(update_fields=["customer"]) + + for service in Service.objects.filter(customer=None): + payments = Payment.objects.filter( + pk__in=service.subscription_set.values_list("payment", flat=True) + ) + if not payments: + print( + f" missing payment {service.status}: {service.site_title} {service.site_url} {service.note}" + ) + continue + service.customer = get_customer( + Customer, payments, invoice_storage, f"Service {service.id}" + ) + service.save(update_fields=["customer"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("weblate_web", "0030_donation_customer_service_customer"), + ] + + operations = [ + migrations.RunPython(update_customer, migrations.RunPython.noop, elidable=True), + ] diff --git a/weblate_web/models.py b/weblate_web/models.py index b71909b834..baa9f28d00 100644 --- a/weblate_web/models.py +++ b/weblate_web/models.py @@ -208,6 +208,7 @@ def create_backup_repository(service): class Donation(models.Model): user = models.ForeignKey(User, on_delete=models.deletion.CASCADE) + customer = models.ForeignKey(Customer, on_delete=models.deletion.CASCADE, null=True) payment = Char32UUIDField(blank=True, null=True) reward = models.IntegerField(choices=REWARDS, default=0) link_text = models.CharField( @@ -301,6 +302,7 @@ def process_donation(payment): # Create new donation = Donation.objects.create( user=user, + customer=payment.customer, payment=payment.pk, reward=int(reward), expires=expires, @@ -319,7 +321,7 @@ def get_service(payment, user): try: return user.service_set.get() except (Service.MultipleObjectsReturned, Service.DoesNotExist): - service = user.service_set.create() + service = user.service_set.create(customer=payment.customer) service.was_created = True return service @@ -496,6 +498,7 @@ def get_repeat(self): class Service(models.Model): secret = models.CharField(max_length=100, default=generate_secret, db_index=True) users = models.ManyToManyField(User) + customer = models.ForeignKey(Customer, on_delete=models.deletion.CASCADE, null=True) status = models.CharField( max_length=150, choices=(