Update billing system

This commit is contained in:
Gregory Trullemans 2024-06-16 11:07:59 +02:00
parent 6bd80f48e4
commit 30287e0ec0
37 changed files with 31011 additions and 313 deletions

View File

@ -1,5 +1,13 @@
# Comptabilité d'indépendant complémentaire # Comptabilité d'indépendant complémentaire
## Installation
Créez un environnement virtuel
`python3 -m venv /path/to/new/virtual/environment`
Installez les requirements
`pip install -r requirements/dev.txt`
## Application de comptabilité ## Application de comptabilité
Elle permet de gérer sa comptabilité (mouvement d'argent) d'indépendant complémentaire. Elle permet de gérer sa comptabilité (mouvement d'argent) d'indépendant complémentaire.

View File

@ -3,32 +3,40 @@ from django.contrib import admin
from .models import ( from .models import (
Client, Client,
Contract, Contract,
DiscountContract,
Prestation, Prestation,
PrestationType,
) )
@admin.register(Client)
class ClientAdmin(admin.ModelAdmin): class ClientAdmin(admin.ModelAdmin):
model = Client model = Client
list_display = ('name', 'address', 'postal_code', 'city', 'contact') list_display = ('name', 'address', 'postal_code', 'city', 'contact')
search_fields = ('name', 'adress', 'city') search_fields = ('name', 'adress', 'city')
@admin.register(Contract)
class ContractAdmin(admin.ModelAdmin): class ContractAdmin(admin.ModelAdmin):
model = Contract model = Contract
list_display = ('name', 'client', 'advance', 'reference', 'date') list_display = ('name', 'client', 'advance', 'reference', 'date')
search_fields = ('name', ) search_fields = ('name', )
list_filter = ('is_finished', ) # 'date__year', list_filter = ('is_finished', ) # 'date__year',
@admin.register(Prestation)
class PrestationAdmin(admin.ModelAdmin): class PrestationAdmin(admin.ModelAdmin):
model = Prestation model = Prestation
list_display = ('date', 'label', 'total_amount_htva', 'total_amount_tvac', 'contract')
list_display = ('date', 'label', 'total_amount', 'contract')
search_fields = ('label', ) search_fields = ('label', )
admin.site.register(Client, ClientAdmin) @admin.register(PrestationType)
admin.site.register(Contract, ContractAdmin) class PrestationTypeAdmin(admin.ModelAdmin):
admin.site.register(Prestation, PrestationAdmin) model = PrestationType
list_display = ('label',)
search_fields = ('label',)
@admin.register(DiscountContract)
class DiscountContractAdmin(admin.ModelAdmin):
model = DiscountContract
list_display = ('contract', 'label', 'quantity', 'unit')

99
billing/forms.py Normal file
View File

@ -0,0 +1,99 @@
import csv
import math
import datetime
from django import forms
from datetime import date
from .models import Client, Contract, Prestation
# from django_select2.forms import Select2MultipleWidget, ModelSelect2Widget
class ClientForm(forms.ModelForm):
class Meta:
model = Client
fields = ("name", "address", "postal_code", "city", "contact", "company_number")
widgets = {
"name": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Nom du client"}
),
"address": forms.TextInput(
attrs={"class": "form-control col-sm-6 col-md-6 col-lg-6 col-xl-6", "placeholder": "Rue et numéro"}
),
"postal_code": forms.TextInput(
attrs={"class": "form-control col-md-2 col-lg-2 col-xl-2", "placeholder": "Code postal"}
),
"city": forms.TextInput(
attrs={"class": "form-control col-md-4 col-lg-4 col-xl-4", "placeholder": "Ville"}
),
"contact": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Nom du contact client"}
),
"is_company": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Routine's long name"}
),
"company_number": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Numéro BCE"}
)
}
class ContractForm(forms.ModelForm):
class Meta:
model = Contract
fields = ("title", "client", "advance", "reference", "is_finished", "is_paid", "description")
client = forms.ModelChoiceField(queryset=Contract.objects.all())
widgets = {
"title": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Titre du contrat"}
),
"client": forms.Select(
attrs={
"class": "form-control"
},
),
"advance": forms.TextInput(
attrs={"class": "form-control", }
),
"reference": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": date.today().strftime("%Y-%m") + "-00x",
"value": date.today().strftime("%Y-%m") + "-00x",
}
),
"is_finished": forms.CheckboxInput(
attrs={"class": "form-control", }
),
"is_paid": forms.CheckboxInput(
attrs={"class": "form-control", }
),
"description": forms.Textarea(
attrs={"class": "form-control", "placeholder": "Description du contract."}
)
}
# class PrestationForm(forms.ModelForm):
# class Meta:
# model = Prestation
# fields = ("contract", "date", "label", "unit", "unit_price")
# widgets = {
# "contract": ModelSelect2Widget(
# search_fields=["contract__icontains",],
# max_results=10,
# attrs={"data-minimum-input-length": 0, "class": "form-control"},
# ),
# "date": forms.DateInput(
# attrs={
# "class": "form-control datepicker",
# "value": date.today().strftime("%Y-%m-%d"),
# }
# ),
# "label": forms.TextInput(
# attrs={"class": "form-control", }
# ),
# "unit": forms.TextInput(
# attrs={"class": "form-control", }
# ),
# "unit_price": forms.TextInput(
# attrs={"class": "form-control", }
# )
# }

View File

@ -0,0 +1,67 @@
# Generated by Django 4.0 on 2024-04-25 17:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Client',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nom')),
('address', models.CharField(max_length=255, verbose_name='Adresse')),
('postal_code', models.IntegerField(verbose_name='Code postal')),
('city', models.CharField(max_length=255, verbose_name='Ville')),
('contact', models.CharField(max_length=255, verbose_name='Personne de contact')),
('email', models.EmailField(max_length=254)),
('phone_number', models.CharField(max_length=20, verbose_name='Téléphone')),
('company_number', models.CharField(blank=True, max_length=50, null=True, verbose_name="N° d'entreprise")),
],
options={
'verbose_name': 'Client',
'verbose_name_plural': 'Clients',
},
),
migrations.CreateModel(
name='Contract',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nom')),
('advance', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=6, verbose_name='Acompte')),
('reference', models.CharField(blank=True, max_length=255, null=True, verbose_name='Référence')),
('is_finished', models.BooleanField(blank=True, default=True)),
('date', models.DateField(auto_now_add=True)),
('invoiced_date', models.DateField(blank=True, default=None, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='get_contract', to='billing.client')),
],
options={
'verbose_name': 'Contrat',
'verbose_name_plural': 'Contrats',
},
),
migrations.CreateModel(
name='Prestation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('label', models.CharField(max_length=255, verbose_name='Libellé')),
('unit', models.DecimalField(decimal_places=2, default=1, max_digits=5, verbose_name='Unité')),
('unit_price', models.DecimalField(decimal_places=2, default='12,5', max_digits=5, verbose_name='Prix unitaire')),
('total_amount', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=6, verbose_name='Prix')),
('contract', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='get_prestation', to='billing.contract')),
],
options={
'verbose_name': 'Prestations',
'verbose_name_plural': 'Prestations',
'ordering': ['date'],
},
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 4.0 on 2024-04-26 13:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('billing', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PrestationType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=255, verbose_name='Libellé')),
],
options={
'verbose_name': 'Type de prestations',
'verbose_name_plural': 'Types de prestations',
},
),
migrations.AddField(
model_name='prestation',
name='duration',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prestation',
name='time_estimated',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prestation',
name='time_tracked',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prestation',
name='tva_value',
field=models.DecimalField(blank=True, decimal_places=2, default=21, max_digits=5, verbose_name='TVA'),
),
migrations.AlterField(
model_name='contract',
name='client',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contracts', to='billing.client'),
),
migrations.AddField(
model_name='prestation',
name='prestation_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='billing.prestationtype'),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 4.0 on 2024-04-27 13:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('billing', '0002_prestationtype_prestation_duration_and_more'),
]
operations = [
migrations.CreateModel(
name='Estimate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nom')),
('date', models.DateField(auto_now_add=True)),
('details', models.TextField()),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='estimates', to='billing.client')),
],
options={
'verbose_name': 'Devis',
'verbose_name_plural': 'Devis',
},
),
migrations.CreateModel(
name='Discount',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=255, verbose_name='Libellé')),
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=5, verbose_name='Quantité')),
('unit', models.PositiveSmallIntegerField(choices=[(0, '%'), (1, '')], verbose_name='Unité')),
('contract', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='discounts', to='billing.contract')),
],
options={
'verbose_name': 'Réduction',
'verbose_name_plural': 'Réductions',
},
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 4.0 on 2024-04-27 14:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0003_estimate_discount'),
]
operations = [
migrations.RemoveField(
model_name='prestation',
name='total_amount',
),
migrations.AddField(
model_name='prestation',
name='total_amount_htva',
field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=6, verbose_name='Prix (htva)'),
),
migrations.AddField(
model_name='prestation',
name='total_amount_tvac',
field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=6, verbose_name='Prix (tvac)'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.0 on 2024-06-16 05:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0004_remove_prestation_total_amount_and_more'),
]
operations = [
migrations.RenameModel(
old_name='Discount',
new_name='DiscountContract',
),
migrations.AlterModelOptions(
name='discountcontract',
options={'verbose_name': 'Réduction contrat', 'verbose_name_plural': 'Réductions contrat'},
),
migrations.CreateModel(
name='DiscountClient',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=255, verbose_name='Libellé')),
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=5, verbose_name='Quantité')),
('unit', models.PositiveSmallIntegerField(choices=[(0, '%'), (1, '')], verbose_name='Unité')),
('client', models.ManyToManyField(to='billing.Client', verbose_name='discounts')),
],
options={
'verbose_name': 'Réduction client',
'verbose_name_plural': 'Réductions clients',
},
),
]

View File

View File

@ -1,6 +1,13 @@
from django.db import models from django.db import models
from django.db.models import Sum
from datetime import date from datetime import date
DISCOUNT_UNIT_CHOICE = [
(0, "%"),
(1, ""),
]
class Client(models.Model): class Client(models.Model):
class Meta: class Meta:
@ -12,8 +19,59 @@ class Client(models.Model):
postal_code = models.IntegerField(verbose_name="Code postal") postal_code = models.IntegerField(verbose_name="Code postal")
city = models.CharField(max_length=255, verbose_name="Ville") city = models.CharField(max_length=255, verbose_name="Ville")
contact = models.CharField(max_length=255, verbose_name="Personne de contact") contact = models.CharField(max_length=255, verbose_name="Personne de contact")
company_number = models.CharField(max_length=50, verbose_name="N° d'entreprise", email = models.EmailField(max_length=254)
blank=True, null=True) phone_number = models.CharField(max_length=20, verbose_name="Téléphone")
company_number = models.CharField(
max_length=50,
verbose_name="N° d'entreprise",
blank=True,
null=True
)
def __str__(self):
return "%s" % (self.name)
class DiscountClient(models.Model):
class Meta:
verbose_name = 'Réduction client'
verbose_name_plural = 'Réductions clients'
label = models.CharField(max_length=255, verbose_name='Libellé')
client = models.ManyToManyField(Client, verbose_name="discounts" )
quantity = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name="Quantité",
default=1
)
unit = models.PositiveSmallIntegerField(
choices=DISCOUNT_UNIT_CHOICE, verbose_name="Unité"
)
def __str__(self):
return f"{self.label} - {self.quantity} {self.unit}"
# def amount_htva(self):
# if self.unit:
# return - self.quantity
# return (self.contract.get_contract_sum("total_amount_htva") * self.quantity / 100)
# def amount_tvac(self):
# if self.unit:
# return self.quantity * 0.21
# return (self.contract.get_contract_sum("total_amount_tvac") * self.quantity / 100)
class Estimate(models.Model):
class Meta:
verbose_name = 'Devis'
verbose_name_plural = 'Devis'
name = models.CharField(max_length=255, verbose_name="Nom")
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="estimates")
date = models.DateField(auto_now_add=True)
details = models.TextField()
def __str__(self): def __str__(self):
return "%s" % (self.name) return "%s" % (self.name)
@ -25,15 +83,24 @@ class Contract(models.Model):
verbose_name_plural = 'Contrats' verbose_name_plural = 'Contrats'
name = models.CharField(max_length=255, verbose_name="Nom") name = models.CharField(max_length=255, verbose_name="Nom")
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="get_contract") client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="contracts")
advance = models.DecimalField(max_digits=6, decimal_places=2, blank=True, advance = models.DecimalField(
verbose_name="Acompte", default=0) max_digits=6,
decimal_places=2,
blank=True,
verbose_name="Acompte",
default=0
)
reference = models.CharField(max_length=255, verbose_name="Référence", blank=True, null=True) reference = models.CharField(max_length=255, verbose_name="Référence", blank=True, null=True)
is_finished = models.BooleanField(default=True, blank=True) is_finished = models.BooleanField(default=True, blank=True)
date = models.DateField(auto_now_add=True) date = models.DateField(auto_now_add=True)
invoiced_date = models.DateField(default=None, blank=True, null=True)
def __str__(self): def __str__(self):
return "%s" % (self.name) return "%s" % (self.name)
def get_contract_sum(self, key):
return self.get_prestation.aggregate(Sum(key))[key + '__sum']
# def __generate_reference(self): # def __generate_reference(self):
# """ # """
@ -55,6 +122,15 @@ class Contract(models.Model):
# self.__generate_reference() # self.__generate_reference()
# super().save(*args, **kwargs) # super().save(*args, **kwargs)
class PrestationType(models.Model):
class Meta:
verbose_name = 'Type de prestations'
verbose_name_plural = 'Types de prestations'
label = models.CharField(max_length=255, verbose_name='Libellé')
def __str__(self):
return f"{self.label}"
class Prestation(models.Model): class Prestation(models.Model):
class Meta: class Meta:
@ -62,21 +138,59 @@ class Prestation(models.Model):
verbose_name_plural = 'Prestations' verbose_name_plural = 'Prestations'
ordering = ['date'] ordering = ['date']
contract = models.ForeignKey(Contract, on_delete=models.CASCADE, related_name="get_prestation", blank=True, null=True) contract = models.ForeignKey(
Contract,
on_delete=models.CASCADE,
related_name="get_prestation",
blank=True,
null=True
)
prestation_type = models.ForeignKey(PrestationType, blank=True, null=True, on_delete=models.SET_NULL)
date = models.DateField() date = models.DateField()
label = models.CharField(max_length=255, verbose_name='Libellé') label = models.CharField(max_length=255, verbose_name='Libellé')
unit = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Unité", unit = models.DecimalField(
default=1) max_digits=5,
unit_price = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Prix unitaire", decimal_places=2,
default="12,5") verbose_name="Unité",
total_amount = models.DecimalField(max_digits=6, decimal_places=2, blank=True, default=1
verbose_name="Prix", default=0) )
unit_price = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name="Prix unitaire",
default="12,5"
)
total_amount_htva = models.DecimalField(
max_digits=6,
decimal_places=2,
blank=True,
verbose_name="Prix (htva)",
default=0
)
tva_value = models.DecimalField(
max_digits=5,
decimal_places=2,
blank=True,
verbose_name="TVA",
default=21
)
total_amount_tvac = models.DecimalField(
max_digits=6,
decimal_places=2,
blank=True,
verbose_name="Prix (tvac)",
default=0
)
duration = models.PositiveIntegerField(null=True, blank=True)
time_estimated = models.PositiveIntegerField(null=True, blank=True)
time_tracked = models.PositiveIntegerField(null=True, blank=True)
def __compute_total_amount(self): def __compute_total_amount(self):
""" """
Calcule le montant total de la prestation. Calcule le montant total de la prestation.
""" """
self.total_amount = self.unit * self.unit_price self.total_amount_htva = self.unit * self.unit_price
self.total_amount_tvac = self.unit * self.unit_price * self.tva_value
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
@ -84,3 +198,40 @@ class Prestation(models.Model):
""" """
self.__compute_total_amount() self.__compute_total_amount()
super().save(*args, **kwargs) super().save(*args, **kwargs)
class DiscountContract(models.Model):
class Meta:
verbose_name = 'Réduction contrat'
verbose_name_plural = 'Réductions contrat'
contract = models.ForeignKey(
Contract,
on_delete=models.CASCADE,
related_name="discounts",
blank=True,
null=True
)
label = models.CharField(max_length=255, verbose_name='Libellé')
quantity = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name="Quantité",
default=1
)
unit = models.PositiveSmallIntegerField(
choices=DISCOUNT_UNIT_CHOICE, verbose_name="Unité"
)
def __str__(self):
return f"{self.label} ({self.contract}) - {self.quantity} {self.unit}"
def amount_htva(self):
if self.unit:
return - self.quantity
return (self.contract.get_contract_sum("total_amount_htva") * self.quantity / 100)
def amount_tvac(self):
if self.unit:
return self.quantity * 0.21
return (self.contract.get_contract_sum("total_amount_tvac") * self.quantity / 100)

View File

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>{% if client_id %}Modification{% else %}Ajout{% endif %} d'un client</h1>
</div>
</div>
<div class="row">
<div class="col-md-6 col-lg-6 col-xl-6 col-md-offset-3 col-lg-offset-3 col-xl-offset-3">
<div class="well well-small text-right">
<form action="{% if client_id %}{% url 'client_update' client_id %}{% else %}{% url 'client_create' %}{% endif %}" method="post" class="form-horizontal" id="formulaire" name="formulaire">
{% csrf_token %}
<div class="form-group row ">
<label for="id_date" class="col-4 col-sm-2 col-md-2 col-lg-2 col-xl-2 control-label">Nom<span class="text-danger">*</span></label>
<div class="col-12 col-sm-10 col-md-8 col-lg-8 col-xl-8 {% if form.name.errors %}has-danger{% endif %}">
{{ form.name }}
{% if form.name.errors %}<span class="btn btn-sm btn-danger-outline">{% for error in form.name.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_gymnast" class="col-4 col-sm-2 col-md-2 col-lg-2 col-xl-2 control-label">Adresse</label>
<div class="col-8 col-sm-8 col-md-6 col-lg-6 col-xl-6 {% if form.address.errors %}has-danger{% endif %}">
{{ form.address }}<br>
{{ form.postal_code }} {{ form.city }}
{% if form.address.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.address.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_skill" class="col-4 col-sm-2 col-md-2 col-lg-2 col-xl-2 control-label">contact</label>
<div class="col-8 col-sm-8 col-md-6 col-lg-6 col-xl-6 {% if form.contact.errors %}has-danger{% endif %}">
{{ form.contact }}
{% if form.contact.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.contact.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_information" class="col-4 col-sm-2 col-md-2 col-lg-2 col-xl-2 control-label">N° BCE</label>
<div class="col-8 col-sm-8 col-md-6 col-lg-6 col-xl-6 {% if form.company_number.errors %}has-danger{% endif %}">
{{ form.company_number }}
{% if form.company_number.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.company_number.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group text-center">
<input type="submit" value="{% if client_id %}Sauvegarder{% else %}Ajouter{% endif %}" class="btn btn-primary" />
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>Détails {{ client.name }}</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<p><b>Nom Client : </b>{{ client.name }}</p>
<p><b>N° BCE : </b>{{ client.company_number }}</p>
<p><b>Nom de contact : </b>{{ client.contact }}</p>
<p><b>Adresse : </b>{{ client.address }} - {{ client.postal_code }} {{ client.city }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -15,6 +15,7 @@
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12"> <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<table class="table table-striped table-bordered table-condensed"> <table class="table table-striped table-bordered table-condensed">
<thead> <thead>
<th></th>
<th>Client</th> <th>Client</th>
<th>Contact</th> <th>Contact</th>
<th>Adresse</th> <th>Adresse</th>
@ -23,7 +24,8 @@
</thead> </thead>
{% for client in client_list %} {% for client in client_list %}
<tr> <tr>
<td>{{ client.name }}</td> <td></td>
<td><a href="{% url 'contract_listing_for_client' client.id %}">{{ client.name }}</a></td>
<td>{{ client.contact }}</td> <td>{{ client.contact }}</td>
<td>{{ client.address }}</td> <td>{{ client.address }}</td>
<td>{{ client.postal_code }}</td> <td>{{ client.postal_code }}</td>
@ -33,7 +35,7 @@
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% block extra_head %}
{{ form.media.css }}
{% endblock %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-md-12 col-lg-12">
<h1>{% if contract_id %}Modification{% else %}Nouveau{% endif %} contract</h1>
</div>
</div>
<div class="row">
<div class="col-md-6 col-l-6 col-xl-6 col-md-offset-3 col-l-offset-3 col-xl-offset-3">
<div class="well well-small text-right">
<form action="{% if contract_id %}{% url 'contract_update' contract_id %}{% else %}{% url 'contract_create' %}{% endif %}" method="post" class="form-horizontal" id="formulaire" name="formulaire">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id }}" class="col-lg-2 control-label">{{ field.label_tag }}</label>
<div class="col-lg-8">
{{ field }}
{% if field.errors %}<span class="btn btn-sm btn-danger-outline">{% for error in field.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
{% endfor %}
<div class="form-group text-center">
<input type="submit" value="{% if client_id %}Sauvegarder{% else %}Modifier{% endif %}" class="btn btn-warning" />
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_script %}
{{ form.media.js }}
{% endblock %}

View File

@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>{{ contract.name }}</h1>
<h4 class="text-muted">{{ contract.client }}</h4>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-10 col-md-offset-1">
{% if prestations_list %}
<table class="table table-striped table-bordered table-condensed">
<thead>
<th>Date</th>
<th>Prestation</th>
<th class="centered">Quantité</th>
<th class="centered">Prix u.</th>
<th class="centered">Total (htva)</th>
<th class="centered">Total (tvac)</th>
</thead>
{% for prestation in prestations_list %}
<tr>
<td>{{ prestation.date|date:"d-m-Y" }}</td>
<td>{{ prestation.label }}</td>
<td class="push-right">{{ prestation.unit }}</td>
<td class="push-right">{{ prestation.unit_price }}</td>
<td class="push-right">{{ prestation.total_amount_htva }} €</td>
<td class="push-right">{{ prestation.total_amount_tvac }} €</td>
</tr>
{% endfor %}
<tr>
<td colspan="4" class="push-right">Total (sans réduction) :</td>
<td class="push-right"><b>{{ total_without_discount_htva|floatformat:2 }} €</b></td>
<td class="push-right"><b>{{ total_without_discount_tvac|floatformat:2 }} €</b></td>
</tr>
{% for discount in contract.discounts.all %}
<tr>
<td colspan="4">{{ discount.label }} (-{{ discount.quantity }}{{ discount.get_unit_display }})</td>
<td class="push-right">-{{ discount.amount_htva|floatformat:2 }} €</td>
<td class="push-right">-{{ discount.amount_tvac|floatformat:2 }} €</td>
</tr>
{% endfor %}
<tr>
<td colspan="4" class="push-right"><b>Total :</b></td>
<td class="push-right"><b>{{ total_with_discount_htva|floatformat:2 }} €</b></td>
<td class="push-right"><b>{{ total_with_discount_tvac|floatformat:2 }} €</b></td>
</tr>
</table>
<p class="right">
<a href="{% url 'contract_export' contract.id %}" class="btn btn-default btn-primary" role="button">Facture PDF</a>
</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -8,16 +8,19 @@
<div id="content"> <div id="content">
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12"> <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>Liste des contrats</h1> <h1>Liste des contrats {% if client %}pour <i>{{ client }}</i>{% endif %}</h1>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12"> <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
{% if contract_list %}
<table class="table table-striped table-bordered table-condensed"> <table class="table table-striped table-bordered table-condensed">
<thead> <thead>
<th class="centered">Année</th> <th class="centered">Année</th>
<th>Nom</th> <th>Nom</th>
{% if not client %}
<th class="centered">Client</th> <th class="centered">Client</th>
{% endif %}
<th class="centered">Acompte</th> <th class="centered">Acompte</th>
<th class="centered"># Prest.</th> <th class="centered"># Prest.</th>
</thead> </thead>
@ -25,15 +28,18 @@
<tr> <tr>
<td class="centered">{{ contract.date.year }}</td> <td class="centered">{{ contract.date.year }}</td>
<td><a href="{% url 'contract_detail' contract.id %}">{{ contract.name }}</a></td> <td><a href="{% url 'contract_detail' contract.id %}">{{ contract.name }}</a></td>
{% if not client %}
<td>{{ contract.client }}</td> <td>{{ contract.client }}</td>
{% endif %}
<td class="push-right">{{ contract.advance }}</td> <td class="push-right">{{ contract.advance }}</td>
<td class="push-right">{{ contract.get_prestation.count }}</td> <td class="push-right">{{ contract.get_prestation.count }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-md-12 col-lg-12">
<h1>{% if prestation_id %}Modification{% else %}Nouvelle{% endif %} prestation</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-10 col-lg-8 col-md-offset-1 col-lg-offset-2">
<div class="well well-small text-right">
<form action="{% if prestation_id %}{% url 'prestation_update' prestation_id %}{% else %}{% url 'prestation_create' %}{% endif %}" method="post" class="form-horizontal" id="formulaire" name="formulaire">
{% csrf_token %}
<div class="form-group row ">
<label for="id_contract" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.contract.label }}</label>
<div class="col-sm-6 col-md-6 col-lg-6 col-xl-6 {% if form.contract.errors %}has-danger{% endif %}">
{{ form.contract }}
{% if form.contract.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.contract.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_date" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.date.label }}</label>
<div class="col-sm-3 col-md-3 col-lg-3 col-xl-3 {% if form.date.errors %}has-danger{% endif %}">
{{ form.date }}
{% if form.date.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.date.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_label" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.label.label }}</label>
<div class="col-sm-6 col-md-6 col-lg-8 col-xl-8 {% if form.label.errors %}has-danger{% endif %}">
{{ form.label }}
{% if form.label.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.label.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_unit" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.unit.label }}</label>
<div class="col-sm-3 col-md-3 col-lg-2 col-xl-2 {% if form.unit.errors %}has-danger{% endif %}">
{{ form.unit }}
{% if form.unit.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.unit.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_unit_price" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.unit_price.label }}</label>
<div class="col-sm-3 col-md-3 col-lg-2 col-xl-2 {% if form.unit_price.errors %}has-danger{% endif %}">
{{ form.unit_price }}
{% if form.unit_price.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.unit_price.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group text-center">
<input type="submit" value="{% if prestation_id %}Modifier{% else %}Sauvegarder{% endif %}" class="btn btn-primary" />
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" >
$(function(){
$('#id_contract_related').autocomplete({
source: function(request, response){
$.ajax({
url: '/billing/contract/lookup/?pattern=' + $('#id_contract_related').val(),
dataType: "json",
success: function(data){
if(data.length != 0){
response($.map(data, function(item){
return {
label: item.Name,
value: item.Name,
contractid: item.ID
}
}))
} else {
response([{ label: 'No result found.', value: '' }]);
};
},
error: function(exception){
console.log(exception);
}
});
},
minLength: 3,
select: function(event, ui){
$($(this).data('ref')).val(ui.item.contractid);
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,137 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="keywords" content="">
<meta name="description" content="">
<meta name="author" content="Gregory Trullemans">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="76x76" href="{% static "img/apple-icon.png" %}">
<link rel="icon" type="image/png" href="{% static "img/favicon.png" %}">
<title>Facture</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@200..900&display=swap" rel="stylesheet" />
<link href="{% static "css/a4_paper.css" %}" rel="stylesheet" />
<link href="{% static "css/black-dashboard_report.css" %}" rel="stylesheet" />
<link href="{% static "css/to_pdf.css" %}" rel="stylesheet" />
</head>
<body class="white-content">
<div class="row no-gutters">
<div id="header-left" class="col-6">
<img src="{% static 'img/visite2.png' %}" style="width:95%" />
</div>
<div id="header-right" class="col-6 text-right pr-0">
<h1 class="main_title">Facture</h1>
</div>
</div>
<br />
<div class="row no-gutters bordered_top bordered_bottom">
<div class="col-6" id="contract_reference">
<p><b>Date d'émission</b> : {{ date|date:"j F Y" }}</p>
</div>
<div class="col-6 text-right">
<p><b>Référence</b> : {{ contract.reference }}</p>
<br />
</div>
</div>
<br />
<div class="row no-gutters" id="client_references">
<div id="header-left" class="col-6 text-left bordered_bottom">
<p><b>Arnaud Croonen</b></p>
<p>Rue landuyt 26, boite 0201</p>
<p>1440 Braine-le-château</p>
<p><b>Mail</b> : info@arnaudcroonen.be</p>
<p><b>Tel</b> : 0472 42 55 56</p>
<p><b>IBAN</b> : BE30 0689 5201 4611</p>
<p><b>TVA</b> : BE 1008.358.946</p>
<br />
</div>
<div id="header-right" class="col-6 text-right bordered_bottom">
<p>à l'attention de <em>{{ client.contact }}</em></p>
<p><b>{{ client.name }}</b></p>
<p>{{ client.address }}</p>
<p>{{ client.postal_code }} {{ client.city }}</p>
<p><b>Mail</b> : wimauto@skynet.be</p>
<p><b>Tel</b> : 0472 42 55 56</p>
<p><b>TVA</b> : {{ client.company_number }}</p>
<br />
</div>
</div>
<br />
<div class="row no-gutters">
<div class="col-12">
<table class="table">
<thead>
<tr class="bordered-row bordered_bottom">
<th class="header text-left pl-0 bordered_bottom" style="width: 35%">Description</th>
<th class="header text-right bordered_bottom" style="width: 15%">Durée</th>
<th class="header text-right bordered_bottom" style="width: 10%">Quantité</th>
<th class="header text-right bordered_bottom" style="width: 12%">Prix u.</th>
<th class="header text-right bordered_bottom" style="width: 11%">Prix (htva)</th>
<th class="header text-right bordered_bottom" style="width: 11%">Prix (tvac)</th>
</tr>
</thead>
<tbody>
{% for prestation in prestations_list %}
<tr>
<td class="pt-2 pb-2 pl-3">{{ prestation.label }}</td>
<td class="pt-2 pb-2 pl-3 text-right">{{ prestation.date|date:"j N Y" }}</td>
<td class="pt-2 pb-2 text-right">{{ prestation.unit }}</td>
<td class="pt-2 pb-2 text-right">{{ prestation.unit_price }}€</td>
<td class="pt-2 pb-2 text-right">{{ prestation.total_amount_htva }} €</td>
<td class="pt-2 pb-2 text-right">{{ prestation.total_amount_tvac }} €</td>
</tr>
{% endfor %}
<tr class="bordered-row">
<td colspan="4" class="push-right">Sous-Total (sans réduction) :</td>
<td class="text-right"><b>{{ total_without_discount_htva|floatformat:2 }} €</b></td>
<td class="text-right"><b>{{ total_without_discount_tvac|floatformat:2 }} €</b></td>
</tr>
{% for discount in contract.discounts.all %}
<tr>
<td colspan="4">{{ discount.label }} (-{{ discount.quantity }}{{ discount.get_unit_display }})</td>
<td class="text-right">-{{ discount.amount_htva|floatformat:2 }} €</td>
<td class="text-right">-{{ discount.amount_tvac|floatformat:2 }} €</td>
</tr>
{% endfor %}
<tfoot>
<tr class="bordered-row">
<td colspan="4" class="pl-0"><strong>Total</strong></td>
<td class="text-right"><b>{{ total_with_discount_htva|floatformat:2 }} €</b></td>
<td class="text-right"><b>{{ total_with_discount_tvac|floatformat:2 }} €</b></td>
</tr>
</tfoot>
</table>
</div>
</div>
<br />
<div class="row no-gutters pdf_footer">
<div class="col-12 bordered_top bordered_bottom bordered_right bordered-left text-center">
<p>Merci de verser l'intégralité de ce montant sur le compte IBAN - <b>BE30 0689 5201 4611</b> dans les 15 jours avec la référence <b>{{ contract.reference }}</b>.</p>
<p>Les produits audiovisuels finaux seront délivrés dès réception du paiement.</p>
<p><em>Merci pour votre confiance.</em></p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,129 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="keywords" content="">
<meta name="description" content="">
<meta name="author" content="Gregory Trullemans">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Facture</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@200..900&display=swap" rel="stylesheet" />
<link href="{% static "css/a4_paper.css" %}" rel="stylesheet" />
<link href="{% static "css/black-dashboard_report.css" %}" rel="stylesheet" />
<link href="{% static "css/to_pdf.css" %}" rel="stylesheet" />
</head>
<header class="white-content">
<div class="row ml-0 mr-0">
<div id="header-left" class="col-6">
<img src="{% static 'img/visite2.png' %}" />
</div>
<div id="header-right" class="col-6 text-right">
<h1 class="main_title">Facture</h1>
</div>
</div>
</header>
<br />
<body class="white-content">
<div class="row ml-0 mr-0">
<div class="col-6 bordered_top" id="contract_reference">
<p><b>Date d'émission</b> : 24/04/2024</p>
</div>
<div class="col-6 text-right bordered_top">
<p><b>Référence</b> : {{ contract.reference }}</p>
</div>
<br />
<br />
</div>
<div class="row ml-0 mr-0" id="client_references">
<div id="header-left" class="col-6 text-left bordered_top">
<p><b>Arnaud Croonen</b></p>
<p>Rue landuyt 26, boite 0201</p>
<p>1440 Braine-le-château</p>
<p><b>Mail</b> : info@arnaudcroonen.be</p>
<p><b>Tel</b> : 0472 42 55 56</p>
<p><b>IBAN</b> : BE30 0689 5201 4611</p>
<p><b>TVA</b> : BE 1008.358.946</p>
</div>
<div id="header-right" class="col-6 text-right bordered_top">
<p>à l'attention de <em>Wim Vanderheyden</em></p>
<p><b>Concept Cycles</b></p>
<p>Route Provinciale, 96</p>
<p>1480 Clabecq</p>
<p><b>Mail</b> : wimauto@skynet.be</p>
<p><b>TVA</b> : 0611.842.841</p>
</div>
</div>
<div class="row ml-0 mr-0">
<div class="col-12">
<table class="table">
<thead>
<tr>
<th>Description</th>
<th>Durée</th>
<th class="text-center">Quantité</th>
<th class="text-center">Prix unitaire</th>
<th class="text-center">Prix</th>
</tr>
</thead>
<tbody>
{% for prestation in prestations_list %}
<tr>
<td class="pt-1 pb-1">{{ prestation.date|date:"d-m-Y" }}</td>
<td class="pt-1 pb-1">{{ prestation.label }}</td>
<td class="pt-1 pb-1 text-right">{{ prestation.unit }}</td>
<td class="pt-1 pb-1 text-right">{{ prestation.unit_price }}€</td>
<td class="pt-1 pb-1 text-right">{{ prestation.total_amount }}€</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="4"><strong>Sous-total (HTVA)</strong></td>
<td class="text-right">{{ total }}€</td>
</tr>
<tr>
<td colspan="4">Réduction "premiers clients" - 10%</td>
<td class="text-right">-50€</td>
</tr>
<tr>
<td colspan="4"><strong>Total HTVA</strong></td>
<td class="text-right">450€</td>
</tr>
<tr>
<td colspan="4">TVA - 21%</td>
<td class="text-right">94.50€</td>
</tr>
<tr>
<td colspan="4"><strong>Total TVAC</strong></td>
<td class="text-right">544.50€</td>
</tr>
</tfoot>
</table>
</div>
</div>
<br />
<div class="row ml-0 mr-0">
<div class="col-12 bordered_top bordered_bottom bordered_right bordered-left text-center">
<p>Merci de verser l'intégralité de ce montant sur le compte IBAN - <b>BE30 0689 5201 4611</b> dans les 15 jours avec la référence <b>202404-001</b>.</p>
<p>Les produits audiovisuels finaux seront délivrés dès réception du paiement.</p>
<p><em>Merci pour votre confiance.</em></p>
</div>
</div>
</body>
</html>

View File

@ -3,8 +3,9 @@ from django.urls import path, re_path
from . import views from . import views
billing_urlpatterns = [ billing_urlpatterns = [
path(r"contract/pdf/<int:contractid>/", views.contract_export, name="contract_export"), path(r"contract/pdf/<int:contract_id>/", views.contract_export, name="contract_export"),
path(r"contract/<int:contractid>/", views.contract_detail, name="contract_detail"), path(r"contract/<int:contract_id>/", views.contract_detail, name="contract_detail"),
path(r"contract/client/<int:client_id>/", views.contract_listing, name="contract_listing_for_client"),
path(r"contract", views.contract_listing, name="contract_listing"), path(r"contract", views.contract_listing, name="contract_listing"),
path(r"prestation", views.prestation_listing, name="prestation_listing"), path(r"prestation", views.prestation_listing, name="prestation_listing"),
path(r"client", views.client_listing, name="client_listing"), path(r"client", views.client_listing, name="client_listing"),

View File

@ -1,14 +1,15 @@
from django.shortcuts import render from django.shortcuts import render, get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from django.http import HttpResponse from django.http import HttpResponse
from django.db.models import Sum
from reportlab.pdfgen import canvas from django.template.loader import render_to_string
from reportlab.lib.colors import Color, black, blue, red
from reportlab.lib.pagesizes import A4 from weasyprint import HTML, CSS
# from weasyprint.fonts import FontConfiguration
# from PIL import Image # from PIL import Image
import os import os
import pendulum
from django.conf import settings from django.conf import settings
from datetime import date from datetime import date
@ -22,31 +23,47 @@ from .models import (
) )
def contract_listing(request): def contract_listing(request, client_id=None):
""" """
Renvoie la liste de tous les contrats. Renvoie la liste de tous les contrats.
""" """
contract_list = Contract.objects.all() if client_id:
context = {'contract_list': contract_list} client = get_object_or_404(Client, pk=client_id)
return render(request, 'billing/contract/listing.html', context) contract_list = client.contracts.all()
else:
client = None
contract_list = Contract.objects.all()
context = {"contract_list": contract_list, "client": client}
return render(request, "contract/listing.html", context)
def contract_detail(request, contractid): def contract_detail(request, contract_id):
""" """
Renvoie toutes les informations relatives à un contrat, en ce y compris les prestations Renvoie toutes les informations relatives à un contrat, en ce y compris les prestations
relatives à celui-ci. relatives à celui-ci.
""" """
contract = Contract.objects.get(pk=contractid) contract = Contract.objects.get(pk=contract_id)
prestation_list = contract.get_prestation.all() total_without_discount_htva = contract.get_contract_sum("total_amount_htva")
prestation_count = prestation_list.count() total_without_discount_tvac = contract.get_contract_sum("total_amount_tvac")
total = list(contract.get_prestation.all().aggregate(Sum('total_amount')).values())[0]
context = {'contract': contract, total_with_discount_htva = total_without_discount_htva
'prestation_list': prestation_list, total_with_discount_tvac = total_without_discount_tvac
'prestation_count': prestation_count, for discount in contract.discounts.all():
'total': total} total_with_discount_htva -= discount.amount_htva()
return render(request, 'billing/contract/detail.html', context) total_with_discount_tvac -= discount.amount_tvac()
context = {
"contract": contract,
"prestations_list": contract.get_prestation.all(),
"total_without_discount_htva": total_without_discount_htva,
"total_without_discount_tvac": total_without_discount_tvac,
"total_with_discount_htva": total_with_discount_htva,
"total_with_discount_tvac": total_with_discount_tvac,
}
return render(request, "contract/detail.html", context)
def client_listing(request): def client_listing(request):
@ -54,8 +71,8 @@ def client_listing(request):
Renvoie la liste de tous les clients. Renvoie la liste de tous les clients.
""" """
client_list = Client.objects.all() client_list = Client.objects.all()
context = {'client_list': client_list} context = {"client_list": client_list}
return render(request, 'billing/client/listing.html', context) return render(request, "client/listing.html", context)
def prestation_listing(request): def prestation_listing(request):
@ -63,229 +80,59 @@ def prestation_listing(request):
Renvoie la liste de toutes les prestations. Renvoie la liste de toutes les prestations.
""" """
prestation_list = Prestation.objects.all() prestation_list = Prestation.objects.all()
context = {'prestation_list': prestation_list} context = {"prestation_list": prestation_list}
return render(request, 'billing/prestation/listing.html', context) return render(request, "prestation/listing.html", context)
Y = 841.89 def contract_export(request, contract_id):
X = 35 """Génere un fichier PDF à fournir au client."""
RIGHT_X = 595.27 - X contract = get_object_or_404(Contract, pk=contract_id)
TITLED_X = 125
INDENTED_X = X + 5
INDENTED_RIGHT_X = RIGHT_X - 5
PRESTATION_COLUMN_2 = INDENTED_X + 65 if not contract.invoiced_date:
PRESTATION_COLUMN_3 = INDENTED_X + 400 today = pendulum.now().date()
PRESTATION_COLUMN_4 = INDENTED_X + 455 contract.invoiced_date = today
contract.save()
COMMON_LINE_HEIGHT = -15 contract = Contract.objects.get(pk=contract_id)
DOUBLE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 2 total_without_discount_htva = contract.get_contract_sum("total_amount_htva")
BIG_LINE_HEIGHT = COMMON_LINE_HEIGHT * 3 total_without_discount_tvac = contract.get_contract_sum("total_amount_tvac")
HUGE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 4
class MyDocument(object): total_with_discount_htva = total_without_discount_htva
# Create the PDF object, using the response object as its "file." total_with_discount_tvac = total_without_discount_tvac
# http://www.reportlab.com/docs/reportlab-userguide.pdf for discount in contract.discounts.all():
# canvas.rect(x, y, width, height, stroke=1, fill=0) total_with_discount_htva -= discount.amount_htva()
# localhost:8000/billing/contract/pdf/2 total_with_discount_tvac -= discount.amount_tvac()
def __init__(self, response): context = {
# Create the PDF object, using the response object as its "file." "contract": contract,
self.document = canvas.Canvas(response, pagesize=A4) "client": contract.client,
self.y = Y - X "date": contract.invoiced_date,
"prestations_list": contract.get_prestation.all(),
# "discounts_list": discounts_list,
# "discount_result": discount_result,
# "prestations_count": prestations_count,
"total_without_discount_htva": total_without_discount_htva,
"total_without_discount_tvac": total_without_discount_tvac,
"total_with_discount_htva": total_with_discount_htva,
"total_with_discount_tvac": total_with_discount_tvac,
}
def newline(self, height=None): # return render(request, "to_pdf/facture.html", context)
"""
Passe à la ligne, la hauteur de la ligne étant passée en paramètre.
"""
if height == DOUBLE_LINE_HEIGHT:
self.y += DOUBLE_LINE_HEIGHT
elif height == BIG_LINE_HEIGHT:
self.y += BIG_LINE_HEIGHT
elif height == HUGE_LINE_HEIGHT:
self.y += HUGE_LINE_HEIGHT
else:
self.y += COMMON_LINE_HEIGHT
# if y < 120; html = render_to_string("to_pdf/facture.html", context)
# document.PageBreak() response = HttpResponse(content_type="application/pdf")
# y = 790 response[
"Content-Disposition"
] = f"attachment; filename=facture.pdf" # pylint: disable=line-too-long
def drawString(self, x, string, font_family="Helvetica", font_decoration=None, font_size=10): # font_config = FontConfiguration()
font = font_family HTML(string=html, base_url=request.build_absolute_uri()).write_pdf(
if font_decoration is not None: response,
font += "-" + font_decoration stylesheets=[
self.document.setFont(font, font_size) CSS(settings.STATICFILES_DIRS[0] + "/css/a4_paper.css"),
self.document.drawString(x, self.y, string) CSS(settings.STATICFILES_DIRS[0] + "/css/black-dashboard_report.css"),
CSS(settings.STATICFILES_DIRS[0] + "/css/to_pdf.css"),
],
) # , font_config=font_config)
def drawNewLine(self, x, string, height=None, font_family="Helvetica", font_decoration=None, font_size=10):
self.newline(height)
self.drawString(x, string, font_family, font_decoration, font_size)
def header(self, info, contract):
"""
Génère le header de la facture.
"""
# self.document.rect(X, 735, 525, 70, fill=0)
self.drawNewLine(INDENTED_X, info['NAME'])
self.document.drawRightString(RIGHT_X, self.y, "N° de Référence : " + str(contract.reference))
self.drawNewLine(INDENTED_X, info['ADDRESS'])
self.drawNewLine(INDENTED_X, info['CITY'])
self.drawNewLine(INDENTED_X, info['GSM'])
self.newline(DOUBLE_LINE_HEIGHT)
def title(self, contract):
"""
Génère le "titre" de la facture.
"""
theString = "A l'attention de"
self.drawString(TITLED_X, "A l'attention de")
self.drawString(194, contract.client.contact, font_decoration="Bold")
self.drawString(194 + (len(contract.client.contact) * 5.2), " pour la")
self.drawNewLine(TITLED_X, contract.client.name, font_decoration="Bold")
self.drawNewLine(TITLED_X, contract.client.address)
self.drawNewLine(TITLED_X, str(contract.client.postal_code) + " " + contract.client.city)
self.newline()
self.drawNewLine(TITLED_X, "Concernant la/le")
self.drawString(200, contract.name, font_decoration="Bold")
self.newline()
def payementInformation(self, info, contract):
"""
Génère les informations de payement.
"""
self.newline()
height = 40
self.document.rect(X, self.y - height, RIGHT_X - X, height, fill=0)
self.drawNewLine(INDENTED_X, "N° Entreprise : " + info['COMPANY_NUMBER'])
self.document.drawRightString(INDENTED_RIGHT_X, self.y, "Votre N° Entreprise : " + contract.client.company_number)
self.drawNewLine(INDENTED_X, "N° de compte : " + info['BANK'] + " - " + info['ACCOUNT'])
self.newline(DOUBLE_LINE_HEIGHT)
def prestations(self, contract):
"""
Génère l'affichage des prestations : tableau, liste des prestations, …
"""
self.drawNewLine(X, "Prestations", font_decoration="Bold")
total = self.drawPrestationsTable(contract.get_prestation.all())
self.newline(DOUBLE_LINE_HEIGHT)
self.document.setFont("Helvetica", 10)
self.document.drawRightString(INDENTED_X + 445, self.y, "Acompte")
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(contract.advance))
self.newline()
self.document.setFont("Helvetica-Bold", 10)
self.document.drawRightString(INDENTED_X + 445, self.y, "Solde à payer")
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(total - contract.advance))
def drawColoredRow(self):
"""
Génère une ligne colorée.
"""
self.document.setFillColorCMYK(0.43,0.2,0,0)
self.document.rect(X, self.y - 4, RIGHT_X - X, COMMON_LINE_HEIGHT, fill=True, stroke=False)
self.document.setFillColorCMYK(0,0,0,1)
def drawHeaderPrestationsTable(self):
"""
Génère le header de la table des prestations.
"""
self.drawColoredRow()
self.newline()
self.document.setFillColorCMYK(0,0,0,1)
self.drawString(INDENTED_X, "Date")
self.drawString(PRESTATION_COLUMN_2, "Libellé")
self.drawString(INDENTED_X + 365, "Nbre")
self.drawString(INDENTED_X + 420, "Prix Unit.")
self.document.setFont("Helvetica-Bold", 10)
self.document.drawRightString(INDENTED_X + 510, self.y, "Total")
def drawFooterPrestationTable(self, total):
"""
Génère le footer de la table des prestations.
"""
self.drawColoredRow()
self.newline()
self.document.setFont("Helvetica-Bold", 10)
self.document.drawRightString(INDENTED_X + 445, self.y, "Total")
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(total))
def displayPrestation(self, prestation, total):
"""
Affiche une ligne de prestations dans le tableau.
"""
# self.newline()
total += prestation.total_amount
self.drawNewLine(INDENTED_X, str(prestation.date))
self.drawString(PRESTATION_COLUMN_2, prestation.label)
self.document.drawRightString(PRESTATION_COLUMN_3, self.y, str(prestation.unit))
# self.document.drawRightString(INDENTED_X + 400, self.y, str(prestation.unit))
self.document.drawRightString(PRESTATION_COLUMN_4, self.y, str(prestation.unit_price))
# self.document.drawRightString(INDENTED_X + 455, self.y, str(prestation.unit_price))
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(prestation.total_amount))
return total
def drawPrestationsTable(self, prestation_list):
"""
Génère le tableau des prestations.
"""
self.drawHeaderPrestationsTable()
total = 0
for prestation in prestation_list:
total = self.displayPrestation(prestation, total)
self.drawFooterPrestationTable(total)
return total
def conclusion(self, info, contract):
"""
Affiche la conclusion de la facture.
"""
self.newline(DOUBLE_LINE_HEIGHT)
self.document.rect(X, self.y, RIGHT_X - X, BIG_LINE_HEIGHT, fill=0)
self.drawNewLine(INDENTED_X, "Merci de bien vouloir payer sur le compte ")
self.drawString(INDENTED_X + 185, info['BANK'] + " : " + info['ACCOUNT'], font_decoration="Bold")
self.newline(COMMON_LINE_HEIGHT)
self.drawString(INDENTED_X, "Avec la référence :")
self.drawString(INDENTED_X + 85, "\"" + str(contract.reference) + "\"", font_decoration="Bold")
def addSignature(self):
"""
Génère la signature.
"""
self.newline(BIG_LINE_HEIGHT)
self.document.drawRightString(RIGHT_X, self.y, "Rebecq, le " + date.today().strftime('%d-%m-%Y'))
self.newline(COMMON_LINE_HEIGHT)
url = os.path.join(settings.STATICFILES_DIRS[0], 'img/signature.png')
self.newline(DOUBLE_LINE_HEIGHT)
im = self.document.drawImage(url, INDENTED_X + 340, self.y, width=180, height=39)
# im.hAlign = 'CENTER'
def download(self):
# Close the PDF object cleanly, and we're done.
self.document.showPage()
self.document.save()
def contract_export(request, contractid):
"""
Génere une fichier PDF pour fournir au client.
"""
info = loadFactureConfig(request)
contract = Contract.objects.get(pk=contractid)
# Create the HttpResponse object with the appropriate PDF headers.
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="facture_' + str(contract.reference) + '.pdf"'
document = MyDocument(response)
document.header(info, contract)
document.title(contract)
document.payementInformation(info, contract)
document.prestations(contract)
document.conclusion(info, contract)
document.addSignature()
document.download()
return response return response

View File

@ -0,0 +1,67 @@
# Generated by Django 4.0 on 2024-04-25 17:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='DescriptionType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nom')),
('year', models.IntegerField(verbose_name='Année')),
('quotity', models.DecimalField(decimal_places=2, max_digits=3, verbose_name='Quotité')),
],
options={
'verbose_name': 'Catégorie',
'verbose_name_plural': 'Catégories',
},
),
migrations.CreateModel(
name='TvaType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(blank=True, max_length=255, verbose_name='Nom')),
('percent', models.DecimalField(decimal_places=3, max_digits=4, verbose_name='Pourcentage')),
('datebegin', models.DateField()),
('dateend', models.DateField(blank=True, null=True)),
],
options={
'verbose_name': 'Type de TVA',
'verbose_name_plural': 'Types de TVA',
},
),
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('information', models.CharField(max_length=255)),
('amountTva', models.DecimalField(blank=True, decimal_places=2, max_digits=5, verbose_name='Montant (TVAC)')),
('amountHTva', models.DecimalField(blank=True, decimal_places=2, max_digits=5, verbose_name='Montant (HTVA)')),
('amountDeductible', models.DecimalField(blank=True, decimal_places=2, max_digits=5, verbose_name='Montant Déductible')),
('paid', models.BooleanField(blank=True, default=False, verbose_name='Payé ?')),
('ticket', models.BooleanField(blank=True, default=False, verbose_name='Ticket ?')),
('notes', models.TextField(blank=True, null=True)),
('quotity', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='Quotité')),
('description', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='TransactionsDesc', to='compta.descriptiontype')),
],
options={
'verbose_name': 'Transaction',
'verbose_name_plural': 'Transactions',
},
),
migrations.AddField(
model_name='descriptiontype',
name='tva_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='get_descriptiontype', to='compta.tvatype'),
),
]

View File

View File

@ -103,7 +103,7 @@ USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATICFILES_DIRS = ( STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'), os.path.join(BASE_DIR, 'static'),
) )
# Default primary key field type # Default primary key field type

Binary file not shown.

10
static/css/a4_paper.css Normal file
View File

@ -0,0 +1,10 @@
@page {
/* A4 : 210mm x 297mm */
size: A4;
/* margin: 1.5cm 1.5cm 1.5cm 1.5cm; */
/* border: 1px solid red; */
}
/* @page :first {
margin: 0.8cm 1cm 1cm 1.3cm;
} */

File diff suppressed because it is too large Load Diff

67
static/css/to_pdf.css Normal file
View File

@ -0,0 +1,67 @@
* {
font-family: "Unbounded";
font-size: 12px;
/* DEBUG OPTION */
/* border: 1px solid black; */
}
.main_title {
margin-top: 10px;
margin-right: -4px;
font-size: 85px;
font-weight: bold;
}
.bordered_top {
border-top: 1px solid black;
}
.bordered_bottom {
border-bottom: 1px solid black;
}
.bordered_right {
border-right: 1px solid black;
}
.bordered-left {
border-left: 1px solid black;
}
td {
color: black !important;
}
.white-content .table>thead>tr>th, .white-content .table {
color: black !important;
}
.white-content .table>tbody>tr>td {
color: black !important;
}
.white-content .table>thead>tr>th {
border-color: black;
border-bottom: 1px solid black !important;
}
.white-content .table>tbody>tr>td {
border: none !important;
}
.white-content .table .bordered-row {
border-color: black;
border-bottom: 1px solid black !important;
}
.pdf_footer {
position: fixed;
bottom: 0cm;
width: 100%;
/* height: 5cm; */
}
/* DEBUG CLASSES */
.bordered {
border: 2px solid black;
}

BIN
static/img/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

BIN
static/img/signature_gt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
static/img/visite2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1,47 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>{{ contract.name }} <small>({{ prestation_count }} prestations)</small></h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-10 col-lg-8 col-md-offset-1 col-lg-offset-2">
<table class="table table-striped table-bordered table-condensed">
<thead>
<th>Date</th>
<th>Prestation</th>
<th class="centered">Unité</th>
<th class="centered">Prix</th>
<th class="centered">Total</th>
</thead>
{% for prestation in prestation_list %}
<tr>
<td>{{ prestation.date|date:"d-m-Y" }}</td>
<td>{{ prestation.label }}</td>
<td class="push-right">{{ prestation.unit }}</td>
<td class="push-right">{{ prestation.unit_price }}</td>
<td class="push-right">{{ prestation.total_amount }}</td>
</tr>
{% endfor %}
<tr>
<td colspan="4" class="push-right"><b>Total :</b></td>
<td class="push-right"><b>{{ total }}</b></td>
</tr>
</table>
<p class="right"><a href="/billing/contract/pdf/{{ contract.id }}" class="btn btn-default btn-primary" role="button">Facture PDF</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}