Update billing system
This commit is contained in:
parent
6bd80f48e4
commit
30287e0ec0
|
@ -1,5 +1,13 @@
|
|||
# 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é
|
||||
Elle permet de gérer sa comptabilité (mouvement d'argent) d'indépendant complémentaire.
|
||||
|
||||
|
|
|
@ -3,32 +3,40 @@ from django.contrib import admin
|
|||
from .models import (
|
||||
Client,
|
||||
Contract,
|
||||
DiscountContract,
|
||||
Prestation,
|
||||
PrestationType,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Client)
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
model = Client
|
||||
|
||||
list_display = ('name', 'address', 'postal_code', 'city', 'contact')
|
||||
search_fields = ('name', 'adress', 'city')
|
||||
|
||||
|
||||
@admin.register(Contract)
|
||||
class ContractAdmin(admin.ModelAdmin):
|
||||
model = Contract
|
||||
|
||||
list_display = ('name', 'client', 'advance', 'reference', 'date')
|
||||
search_fields = ('name', )
|
||||
list_filter = ('is_finished', ) # 'date__year',
|
||||
|
||||
|
||||
@admin.register(Prestation)
|
||||
class PrestationAdmin(admin.ModelAdmin):
|
||||
model = Prestation
|
||||
|
||||
list_display = ('date', 'label', 'total_amount', 'contract')
|
||||
list_display = ('date', 'label', 'total_amount_htva', 'total_amount_tvac', 'contract')
|
||||
search_fields = ('label', )
|
||||
|
||||
|
||||
admin.site.register(Client, ClientAdmin)
|
||||
admin.site.register(Contract, ContractAdmin)
|
||||
admin.site.register(Prestation, PrestationAdmin)
|
||||
@admin.register(PrestationType)
|
||||
class PrestationTypeAdmin(admin.ModelAdmin):
|
||||
model = PrestationType
|
||||
list_display = ('label',)
|
||||
search_fields = ('label',)
|
||||
|
||||
@admin.register(DiscountContract)
|
||||
class DiscountContractAdmin(admin.ModelAdmin):
|
||||
model = DiscountContract
|
||||
list_display = ('contract', 'label', 'quantity', 'unit')
|
||||
|
|
|
@ -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", }
|
||||
# )
|
||||
# }
|
|
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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)'),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,6 +1,13 @@
|
|||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
|
||||
from datetime import date
|
||||
|
||||
DISCOUNT_UNIT_CHOICE = [
|
||||
(0, "%"),
|
||||
(1, "€"),
|
||||
]
|
||||
|
||||
|
||||
class Client(models.Model):
|
||||
class Meta:
|
||||
|
@ -12,8 +19,59 @@ class Client(models.Model):
|
|||
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")
|
||||
company_number = models.CharField(max_length=50, verbose_name="N° d'entreprise",
|
||||
blank=True, null=True)
|
||||
email = models.EmailField(max_length=254)
|
||||
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):
|
||||
return "%s" % (self.name)
|
||||
|
@ -25,16 +83,25 @@ class Contract(models.Model):
|
|||
verbose_name_plural = 'Contrats'
|
||||
|
||||
name = models.CharField(max_length=255, verbose_name="Nom")
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="get_contract")
|
||||
advance = models.DecimalField(max_digits=6, decimal_places=2, blank=True,
|
||||
verbose_name="Acompte", default=0)
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="contracts")
|
||||
advance = models.DecimalField(
|
||||
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)
|
||||
is_finished = models.BooleanField(default=True, blank=True)
|
||||
date = models.DateField(auto_now_add=True)
|
||||
invoiced_date = models.DateField(default=None, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % (self.name)
|
||||
|
||||
def get_contract_sum(self, key):
|
||||
return self.get_prestation.aggregate(Sum(key))[key + '__sum']
|
||||
|
||||
# def __generate_reference(self):
|
||||
# """
|
||||
# Génère automatiquement la référence du contract
|
||||
|
@ -55,6 +122,15 @@ class Contract(models.Model):
|
|||
# self.__generate_reference()
|
||||
# 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 Meta:
|
||||
|
@ -62,21 +138,59 @@ class Prestation(models.Model):
|
|||
verbose_name_plural = 'Prestations'
|
||||
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()
|
||||
label = models.CharField(max_length=255, verbose_name='Libellé')
|
||||
unit = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Unité",
|
||||
default=1)
|
||||
unit_price = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Prix unitaire",
|
||||
default="12,5")
|
||||
total_amount = models.DecimalField(max_digits=6, decimal_places=2, blank=True,
|
||||
verbose_name="Prix", default=0)
|
||||
unit = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
verbose_name="Unité",
|
||||
default=1
|
||||
)
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
|
@ -84,3 +198,40 @@ class Prestation(models.Model):
|
|||
"""
|
||||
self.__compute_total_amount()
|
||||
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)
|
||||
|
|
|
@ -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 %} <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 %} <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 %} <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 %}
|
|
@ -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 %}
|
|
@ -15,6 +15,7 @@
|
|||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>Client</th>
|
||||
<th>Contact</th>
|
||||
<th>Adresse</th>
|
||||
|
@ -23,7 +24,8 @@
|
|||
</thead>
|
||||
{% for client in client_list %}
|
||||
<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.address }}</td>
|
||||
<td>{{ client.postal_code }}</td>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -8,16 +8,19 @@
|
|||
<div id="content">
|
||||
<div class="row">
|
||||
<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 class="row">
|
||||
<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">
|
||||
<thead>
|
||||
<th class="centered">Année</th>
|
||||
<th>Nom</th>
|
||||
{% if not client %}
|
||||
<th class="centered">Client</th>
|
||||
{% endif %}
|
||||
<th class="centered">Acompte</th>
|
||||
<th class="centered"># Prest.</th>
|
||||
</thead>
|
||||
|
@ -25,12 +28,15 @@
|
|||
<tr>
|
||||
<td class="centered">{{ contract.date.year }}</td>
|
||||
<td><a href="{% url 'contract_detail' contract.id %}">{{ contract.name }}</a></td>
|
||||
{% if not client %}
|
||||
<td>{{ contract.client }}</td>
|
||||
{% endif %}
|
||||
<td class="push-right">{{ contract.advance }}</td>
|
||||
<td class="push-right">{{ contract.get_prestation.count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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 %} <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 %} <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 %} <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 %} <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 %} <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 %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -3,8 +3,9 @@ from django.urls import path, re_path
|
|||
from . import views
|
||||
|
||||
billing_urlpatterns = [
|
||||
path(r"contract/pdf/<int:contractid>/", views.contract_export, name="contract_export"),
|
||||
path(r"contract/<int:contractid>/", views.contract_detail, name="contract_detail"),
|
||||
path(r"contract/pdf/<int:contract_id>/", views.contract_export, name="contract_export"),
|
||||
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"prestation", views.prestation_listing, name="prestation_listing"),
|
||||
path(r"client", views.client_listing, name="client_listing"),
|
||||
|
|
317
billing/views.py
317
billing/views.py
|
@ -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.http import HttpResponse
|
||||
from django.db.models import Sum
|
||||
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.colors import Color, black, blue, red
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from weasyprint import HTML, CSS
|
||||
# from weasyprint.fonts import FontConfiguration
|
||||
|
||||
# from PIL import Image
|
||||
import os
|
||||
import pendulum
|
||||
from django.conf import settings
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
if client_id:
|
||||
client = get_object_or_404(Client, pk=client_id)
|
||||
contract_list = client.contracts.all()
|
||||
else:
|
||||
client = None
|
||||
contract_list = Contract.objects.all()
|
||||
context = {'contract_list': contract_list}
|
||||
return render(request, 'billing/contract/listing.html', context)
|
||||
|
||||
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
|
||||
relatives à celui-ci.
|
||||
"""
|
||||
|
||||
contract = Contract.objects.get(pk=contractid)
|
||||
prestation_list = contract.get_prestation.all()
|
||||
prestation_count = prestation_list.count()
|
||||
total = list(contract.get_prestation.all().aggregate(Sum('total_amount')).values())[0]
|
||||
context = {'contract': contract,
|
||||
'prestation_list': prestation_list,
|
||||
'prestation_count': prestation_count,
|
||||
'total': total}
|
||||
return render(request, 'billing/contract/detail.html', context)
|
||||
contract = Contract.objects.get(pk=contract_id)
|
||||
total_without_discount_htva = contract.get_contract_sum("total_amount_htva")
|
||||
total_without_discount_tvac = contract.get_contract_sum("total_amount_tvac")
|
||||
|
||||
total_with_discount_htva = total_without_discount_htva
|
||||
total_with_discount_tvac = total_without_discount_tvac
|
||||
for discount in contract.discounts.all():
|
||||
total_with_discount_htva -= discount.amount_htva()
|
||||
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):
|
||||
|
@ -54,8 +71,8 @@ def client_listing(request):
|
|||
Renvoie la liste de tous les clients.
|
||||
"""
|
||||
client_list = Client.objects.all()
|
||||
context = {'client_list': client_list}
|
||||
return render(request, 'billing/client/listing.html', context)
|
||||
context = {"client_list": client_list}
|
||||
return render(request, "client/listing.html", context)
|
||||
|
||||
|
||||
def prestation_listing(request):
|
||||
|
@ -63,229 +80,59 @@ def prestation_listing(request):
|
|||
Renvoie la liste de toutes les prestations.
|
||||
"""
|
||||
prestation_list = Prestation.objects.all()
|
||||
context = {'prestation_list': prestation_list}
|
||||
return render(request, 'billing/prestation/listing.html', context)
|
||||
context = {"prestation_list": prestation_list}
|
||||
return render(request, "prestation/listing.html", context)
|
||||
|
||||
|
||||
Y = 841.89
|
||||
X = 35
|
||||
RIGHT_X = 595.27 - X
|
||||
TITLED_X = 125
|
||||
INDENTED_X = X + 5
|
||||
INDENTED_RIGHT_X = RIGHT_X - 5
|
||||
def contract_export(request, contract_id):
|
||||
"""Génere un fichier PDF à fournir au client."""
|
||||
contract = get_object_or_404(Contract, pk=contract_id)
|
||||
|
||||
PRESTATION_COLUMN_2 = INDENTED_X + 65
|
||||
PRESTATION_COLUMN_3 = INDENTED_X + 400
|
||||
PRESTATION_COLUMN_4 = INDENTED_X + 455
|
||||
if not contract.invoiced_date:
|
||||
today = pendulum.now().date()
|
||||
contract.invoiced_date = today
|
||||
contract.save()
|
||||
|
||||
COMMON_LINE_HEIGHT = -15
|
||||
DOUBLE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 2
|
||||
BIG_LINE_HEIGHT = COMMON_LINE_HEIGHT * 3
|
||||
HUGE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 4
|
||||
contract = Contract.objects.get(pk=contract_id)
|
||||
total_without_discount_htva = contract.get_contract_sum("total_amount_htva")
|
||||
total_without_discount_tvac = contract.get_contract_sum("total_amount_tvac")
|
||||
|
||||
class MyDocument(object):
|
||||
# Create the PDF object, using the response object as its "file."
|
||||
# http://www.reportlab.com/docs/reportlab-userguide.pdf
|
||||
# canvas.rect(x, y, width, height, stroke=1, fill=0)
|
||||
# localhost:8000/billing/contract/pdf/2
|
||||
total_with_discount_htva = total_without_discount_htva
|
||||
total_with_discount_tvac = total_without_discount_tvac
|
||||
for discount in contract.discounts.all():
|
||||
total_with_discount_htva -= discount.amount_htva()
|
||||
total_with_discount_tvac -= discount.amount_tvac()
|
||||
|
||||
def __init__(self, response):
|
||||
# Create the PDF object, using the response object as its "file."
|
||||
self.document = canvas.Canvas(response, pagesize=A4)
|
||||
self.y = Y - X
|
||||
context = {
|
||||
"contract": contract,
|
||||
"client": contract.client,
|
||||
"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):
|
||||
"""
|
||||
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
|
||||
# return render(request, "to_pdf/facture.html", context)
|
||||
|
||||
# if y < 120;
|
||||
# document.PageBreak()
|
||||
# y = 790
|
||||
html = render_to_string("to_pdf/facture.html", context)
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
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 = font_family
|
||||
if font_decoration is not None:
|
||||
font += "-" + font_decoration
|
||||
self.document.setFont(font, font_size)
|
||||
self.document.drawString(x, self.y, string)
|
||||
# font_config = FontConfiguration()
|
||||
HTML(string=html, base_url=request.build_absolute_uri()).write_pdf(
|
||||
response,
|
||||
stylesheets=[
|
||||
CSS(settings.STATICFILES_DIRS[0] + "/css/a4_paper.css"),
|
||||
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
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
@ -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
|
@ -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;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
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 |
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
|
@ -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 %}
|
Loading…
Reference in New Issue