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):
list_filter = (AdherentTypeFilter, ActiveFilter, AdhesionImportedFilter)
list_select_related = ('user', 'user__profile', 'corporation',)
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',) \
+ 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])
......
import io
from functools import update_wrapper
from django.conf.urls import url
from django.contrib import admin
from django.core.exceptions import PermissionDenied
from django.db import models
from django.urls import path, reverse
from django.forms import BaseInlineFormSet
from django.http import FileResponse, Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import path, reverse
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 adhesions.admin import AdhesionAdmin
from djadhere.utils import ActiveFilter
from services.admin import ServiceAdmin
from services.models import ServiceType
from services.models import Service
from .facture import facture
from .models import Expense, Invoice, InvoicedProduct, PaymentUpdate, RecurringPayment
from .utils import notify_payment_update
### Inlines
class PaymentMethodFilter(admin.SimpleListFilter):
......@@ -313,16 +317,25 @@ class InvoicedProductAdmin(admin.TabularInline):
class InvoiceAdmin(admin.ModelAdmin):
inlines = (InvoicedProductAdmin,)
list_display = ('reference', 'date', 'buyer', 'amount', 'invoiced',)
fields = ('date', 'reference', 'buyer', 'notes',)
list_display = ('reference', 'date', 'adhesion_link', 'total', 'invoiced',)
raw_id_fields = ('buyer',)
list_filter = ('date',)
search_fields = ('reference', '=buyer__id',)
search_fields = ('=num', '=buyer__id',)
def amount(self, invoice):
return sum(map(lambda item: item.quantity * item.price, invoice.items.all()))
amount.short_description = 'Montant'
def get_queryset(self, request):
qs = super().get_queryset(request)
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):
return bool(invoice.pdf)
......@@ -330,11 +343,59 @@ class InvoiceAdmin(admin.ModelAdmin):
invoiced.boolean = True
def get_readonly_fields(self, request, obj=None):
if obj and obj.pdf:
return ('date', 'reference', 'buyer',)
if obj:
return ('date', 'reference', 'adhesion_link', 'total', 'membership', 'services',)
else:
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):
if not obj or obj.pdf:
return False
......@@ -353,7 +414,10 @@ class InvoiceAdmin(admin.ModelAdmin):
p = canvas.Canvas(buffer)
produits = []
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
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)
......
# 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):
class Invoice(models.Model):
created = models.DateTimeField(auto_now_add=True)
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')
notes = models.TextField(blank=True, default='')
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):
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:
verbose_name = 'facture'
verbose_name_plural = 'factures'
ordering = ('-reference',)
ordering = ('-date', '-num',)
def __str__(self):
return self.reference
......
......@@ -17,6 +17,7 @@ from django.contrib.humanize.templatetags.humanize import naturaltime
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
#from djgeojson.views import GeoJSONLayerView
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