Commit 79682f73 authored by Élie Bouttier's avatar Élie Bouttier
Browse files

improve billing

parent f1e98e68
...@@ -241,7 +241,7 @@ class AdhesionAdmin(AdtSearchMixin, admin.ModelAdmin): ...@@ -241,7 +241,7 @@ class AdhesionAdmin(AdtSearchMixin, admin.ModelAdmin):
list_filter = (AdherentTypeFilter, ActiveFilter, AdhesionImportedFilter) list_filter = (AdherentTypeFilter, ActiveFilter, AdhesionImportedFilter)
list_select_related = ('user', 'user__profile', 'corporation',) list_select_related = ('user', 'user__profile', 'corporation',)
fields = ('id', 'type', 'get_adherent_link', 'get_membership_link',) fields = ('id', 'type', 'get_adherent_link', 'get_membership_link',)
readonly_fields = ('id', 'type', 'get_adherent_link', 'get_membership_link', 'get_antennas_link',) readonly_fields = ('id', 'type', 'get_adherent_link', 'get_membership_link',)# 'get_antennas_link',)
search_fields = ('=id', 'notes',) \ search_fields = ('=id', 'notes',) \
+ tuple(['user__%s' % f for f in UserAdmin.search_fields if 'adhesion' not in f]) \ + tuple(['user__%s' % f for f in UserAdmin.search_fields if 'adhesion' not in f]) \
+ tuple(['corporation__%s' % f for f in CorporationAdmin.search_fields if 'adhesion' not in f]) + tuple(['corporation__%s' % f for f in CorporationAdmin.search_fields if 'adhesion' not in f])
......
import io
from functools import update_wrapper
from django.conf.urls import url from django.conf.urls import url
from django.contrib import admin from django.contrib import admin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.urls import path, reverse
from django.forms import BaseInlineFormSet from django.forms import BaseInlineFormSet
from django.http import FileResponse, Http404 from django.http import FileResponse, Http404
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import path, reverse from django.urls import path, reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.db.models import F, Sum, ExpressionWrapper, Subquery
from django.db.models.functions import Coalesce
from django.utils.safestring import mark_safe
import io
from functools import update_wrapper
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from adhesions.admin import AdhesionAdmin from adhesions.admin import AdhesionAdmin
from djadhere.utils import ActiveFilter from djadhere.utils import ActiveFilter
from services.admin import ServiceAdmin from services.admin import ServiceAdmin
from services.models import ServiceType from services.models import Service
from .facture import facture from .facture import facture
from .models import Expense, Invoice, InvoicedProduct, PaymentUpdate, RecurringPayment from .models import Expense, Invoice, InvoicedProduct, PaymentUpdate, RecurringPayment
from .utils import notify_payment_update from .utils import notify_payment_update
### Inlines ### Inlines
class PaymentMethodFilter(admin.SimpleListFilter): class PaymentMethodFilter(admin.SimpleListFilter):
...@@ -313,16 +317,25 @@ class InvoicedProductAdmin(admin.TabularInline): ...@@ -313,16 +317,25 @@ class InvoicedProductAdmin(admin.TabularInline):
class InvoiceAdmin(admin.ModelAdmin): class InvoiceAdmin(admin.ModelAdmin):
inlines = (InvoicedProductAdmin,) list_display = ('reference', 'date', 'adhesion_link', 'total', 'invoiced',)
list_display = ('reference', 'date', 'buyer', 'amount', 'invoiced',)
fields = ('date', 'reference', 'buyer', 'notes',)
raw_id_fields = ('buyer',) raw_id_fields = ('buyer',)
list_filter = ('date',) list_filter = ('date',)
search_fields = ('reference', '=buyer__id',) search_fields = ('=num', '=buyer__id',)
def amount(self, invoice): def get_queryset(self, request):
return sum(map(lambda item: item.quantity * item.price, invoice.items.all())) qs = super().get_queryset(request)
amount.short_description = 'Montant' qs = qs.annotate(
total=Coalesce(Sum(ExpressionWrapper(
F('items__quantity') * F('items__price'),
output_field=models.DecimalField(decimal_places=2)
)), 0)
)
return qs
def total(self, invoice):
return '%.2f €' % invoice.total
total.short_description = 'Montant'
total.admin_order_field = 'total'
def invoiced(self, invoice): def invoiced(self, invoice):
return bool(invoice.pdf) return bool(invoice.pdf)
...@@ -330,11 +343,59 @@ class InvoiceAdmin(admin.ModelAdmin): ...@@ -330,11 +343,59 @@ class InvoiceAdmin(admin.ModelAdmin):
invoiced.boolean = True invoiced.boolean = True
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
if obj and obj.pdf: if obj:
return ('date', 'reference', 'buyer',) return ('date', 'reference', 'adhesion_link', 'total', 'membership', 'services',)
else: else:
return [] return []
def get_fieldsets(self, request, obj=None):
fieldsets = []
if obj:
fieldsets += [
(None, {
'fields': ('date', 'reference', 'adhesion_link', 'total', 'notes'),
})
]
if not obj.pdf:
fieldsets += [
('Voir les cotisation et services en cours', {
'fields': ('membership', 'services'),
'classes': ('collapse',),
})
]
else:
fieldsets += [
(None, {
'fields': ('buyer',),
})
]
return fieldsets
def get_inlines(self, request, obj=None):
if obj:
return (InvoicedProductAdmin,)
else:
return ()
def membership(self, obj):
return format_html(u'<a href="{}">{}</a>', obj.buyer.membership.get_absolute_url(), obj.buyer.membership.get_current_payment_display())
membership.short_description = 'Cotisation'
def services(self, obj):
services = Service.objects.filter(adhesion=obj.buyer, active=True)
services_display = []
for service in services:
description = format_html(u'<a href="{}">{}</a>', service.get_absolute_url(), service)
contribution = format_html(u'<a href="{}">{}</a>', service.contribution.get_absolute_url(), service.contribution.get_current_payment_display())
services_display.append('{}: {}'.format(description, contribution))
return mark_safe('<br/>'.join(services_display))
services.short_description = 'Services'
def adhesion_link(self, obj):
url = reverse('admin:adhesions_adhesion_change', args=[obj.buyer.pk])
return format_html('<a href="{}">{}</a>', url, str(obj.buyer))
adhesion_link.short_description = 'Adhérent'
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
if not obj or obj.pdf: if not obj or obj.pdf:
return False return False
...@@ -353,7 +414,10 @@ class InvoiceAdmin(admin.ModelAdmin): ...@@ -353,7 +414,10 @@ class InvoiceAdmin(admin.ModelAdmin):
p = canvas.Canvas(buffer) p = canvas.Canvas(buffer)
produits = [] produits = []
for item in invoice.items.all(): for item in invoice.items.all():
produits += [(item.description, item.notes, item.quantity, item.price)] description = item.description
if item.notes:
description += '\n' + item.notes
produits += [(description, item.quantity, item.price)]
adt = invoice.buyer.adherent adt = invoice.buyer.adherent
facture(p, dt=invoice.date, ref=invoice.reference, produits=produits, adt=str(adt), mail=adt.email, facture(p, dt=invoice.date, ref=invoice.reference, produits=produits, adt=str(adt), mail=adt.email,
tel=adt.phone_number, addr=adt.address, draft=draft) tel=adt.phone_number, addr=adt.address, draft=draft)
......
# Generated by Django 3.0.5 on 2020-06-08 22:01
import re
from django.db import migrations, models
def set_num(apps, schema_editor):
db_alias = schema_editor.connection.alias
Invoice = apps.get_model('banking', 'Invoice')
regex = re.compile('F([0-9]{8})-([0-9]{4})-ADT([0-9]{1,4})')
for invoice in Invoice.objects.all():
g = regex.match(invoice.reference)
invoice.num = int(g.group(2))
invoice.save()
class Migration(migrations.Migration):
dependencies = [
('banking', '0014_invoicedproduct_notes'),
]
operations = [
migrations.AlterModelOptions(
name='invoice',
options={'ordering': ('-date', '-num'), 'verbose_name': 'facture', 'verbose_name_plural': 'factures'},
),
migrations.AlterField(
model_name='invoice',
name='reference',
field=models.CharField(max_length=32, verbose_name='Référence', null=True),
),
migrations.AddField(
model_name='invoice',
name='num',
field=models.IntegerField(null=True),
),
migrations.RunPython(set_num),
migrations.RemoveField(
model_name='invoice',
name='reference',
),
migrations.AlterField(
model_name='invoice',
name='num',
field=models.IntegerField(),
),
]
...@@ -170,18 +170,29 @@ class Expense(models.Model): ...@@ -170,18 +170,29 @@ class Expense(models.Model):
class Invoice(models.Model): class Invoice(models.Model):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
date = models.DateField(verbose_name='Date', default=datetime.date.today) date = models.DateField(verbose_name='Date', default=datetime.date.today)
reference = models.CharField(max_length=32, verbose_name='Référence', unique=True) num = models.IntegerField()
buyer = models.ForeignKey('adhesions.Adhesion', related_name='invoices', on_delete=models.PROTECT, verbose_name='Adhérent') buyer = models.ForeignKey('adhesions.Adhesion', related_name='invoices', on_delete=models.PROTECT, verbose_name='Adhérent')
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, default='')
pdf = models.BinaryField(null=True) pdf = models.BinaryField(null=True)
@property
def reference(self):
return 'F{:04d}{:02d}{:02d}-{:04d}-ADT{}'.format(self.date.year, self.date.month, self.date.day, self.num, self.buyer.pk)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=(self.pk,)) return reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=(self.pk,))
def save(self, *args, **kwargs):
if not self.pk:
self.date = datetime.date.today()
last_invoice = Invoice.objects.filter(date__year=self.date.year).order_by('num').last()
self.num = last_invoice.num + 1 if last_invoice is not None else 1
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = 'facture' verbose_name = 'facture'
verbose_name_plural = 'factures' verbose_name_plural = 'factures'
ordering = ('-reference',) ordering = ('-date', '-num',)
def __str__(self): def __str__(self):
return self.reference return self.reference
......
...@@ -17,6 +17,7 @@ from django.contrib.humanize.templatetags.humanize import naturaltime ...@@ -17,6 +17,7 @@ from django.contrib.humanize.templatetags.humanize import naturaltime
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
#from djgeojson.views import GeoJSONLayerView #from djgeojson.views import GeoJSONLayerView
from urllib.parse import urlencode from urllib.parse import urlencode
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment