8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-22 03:13:12 +00:00

Add Cost Estimates

This commit is contained in:
Hugo LEVY-FALK 2018-12-31 23:58:37 +01:00 committed by chirac
parent b85384b226
commit 37dbfd2fbf
13 changed files with 511 additions and 9 deletions

View file

@ -30,7 +30,7 @@ from django.contrib import admin
from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
from .models import CustomInvoice
from .models import CustomInvoice, CostEstimate
class FactureAdmin(VersionAdmin):
@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin):
pass
class CostEstimateAdmin(VersionAdmin):
"""Admin class for cost estimates."""
pass
class CustomInvoiceAdmin(VersionAdmin):
"""Admin class for custom invoices."""
pass
@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin)
admin.site.register(Vente, VenteAdmin)
admin.site.register(Cotisation, CotisationAdmin)
admin.site.register(CustomInvoice, CustomInvoiceAdmin)
admin.site.register(CostEstimate, CostEstimateAdmin)

View file

@ -46,7 +46,10 @@ from django.shortcuts import get_object_or_404
from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin
from .models import Article, Paiement, Facture, Banque, CustomInvoice, Vente
from .models import (
Article, Paiement, Facture, Banque,
CustomInvoice, Vente, CostEstimate
)
from .payment_methods import balance
@ -153,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm):
fields = '__all__'
class CostEstimateForm(FormRevMixin, ModelForm):
"""
Form used to create a cost estimate.
"""
class Meta:
model = CostEstimate
exclude = ['paid', 'final_invoice']
class ArticleForm(FormRevMixin, ModelForm):
"""
Form used to create an article.

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-29 21:03
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0036_custominvoice_remark'),
]
operations = [
migrations.CreateModel(
name='CostEstimate',
fields=[
('custominvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.CustomInvoice')),
('validity', models.DurationField(verbose_name='Period of validity')),
('final_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='origin_cost_estimate', to='cotisations.CustomInvoice')),
],
options={
'permissions': (('view_costestimate', 'Can view a cost estimate object'),),
},
bases=('cotisations.custominvoice',),
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-31 22:57
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0037_costestimate'),
]
operations = [
migrations.AlterField(
model_name='costestimate',
name='final_invoice',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_cost_estimate', to='cotisations.CustomInvoice'),
),
migrations.AlterField(
model_name='costestimate',
name='validity',
field=models.DurationField(help_text='DD HH:MM:SS', verbose_name='Period of validity'),
),
migrations.AlterField(
model_name='custominvoice',
name='paid',
field=models.BooleanField(default=False, verbose_name='Paid'),
),
]

View file

@ -284,7 +284,8 @@ class CustomInvoice(BaseInvoice):
verbose_name=_("Address")
)
paid = models.BooleanField(
verbose_name=_("Paid")
verbose_name=_("Paid"),
default=False
)
remark = models.TextField(
verbose_name=_("Remark"),
@ -293,6 +294,57 @@ class CustomInvoice(BaseInvoice):
)
class CostEstimate(CustomInvoice):
class Meta:
permissions = (
('view_costestimate', _("Can view a cost estimate object")),
)
validity = models.DurationField(
verbose_name=_("Period of validity"),
help_text="DD HH:MM:SS"
)
final_invoice = models.ForeignKey(
CustomInvoice,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="origin_cost_estimate",
primary_key=False
)
def create_invoice(self):
"""Create a CustomInvoice from the CostEstimate."""
if self.final_invoice is not None:
return self.final_invoice
invoice = CustomInvoice()
invoice.recipient = self.recipient
invoice.payment = self.payment
invoice.address = self.address
invoice.paid = False
invoice.remark = self.remark
invoice.date = timezone.now()
invoice.save()
self.final_invoice = invoice
self.save()
for sale in self.vente_set.all():
Vente.objects.create(
facture=invoice,
name=sale.name,
prix=sale.prix,
number=sale.number,
)
return invoice
def can_delete(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.delete_costestimate'):
return False, _("You don't have the right "
"to delete a cost estimate.")
if self.final_invoice is not None:
return False, _("The cost estimate has an "
"invoice and cannot be deleted.")
return True, None
# TODO : change Vente to Purchase
class Vente(RevMixin, AclMixin, models.Model):
"""

View file

@ -0,0 +1,101 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2018 Hugo Levy-Falk
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load i18n %}
{% load acl %}
{% load logs_extra %}
{% load design %}
<div class="table-responsive">
{% if cost_estimate_list.paginator %}
{% include 'pagination.html' with list=cost_estimate_list%}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>
{% trans "Recipient" as tr_recip %}
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %}
</th>
<th>{% trans "Designation" %}</th>
<th>{% trans "Total price" %}</th>
<th>
{% trans "Payment method" as tr_payment_method %}
{% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %}
</th>
<th>
{% trans "Date" as tr_date %}
{% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %}
</th>
<th>
{% trans "Validity" as tr_validity %}
{% include 'buttons/sort.html' with prefix='invoice' col='validity' text=tr_validity %}
</th>
<th>
{% trans "Cost estimate ID" as tr_estimate_id %}
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_estimate_id %}
</th>
<th>
{% trans "Invoice created" as tr_invoice_created%}
{% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_created %}
</th>
<th></th>
<th></th>
</tr>
</thead>
{% for estimate in cost_estimate_list %}
<tr>
<td>{{ estimate.recipient }}</td>
<td>{{ estimate.name }}</td>
<td>{{ estimate.prix_total }}</td>
<td>{{ estimate.payment }}</td>
<td>{{ estimate.date }}</td>
<td>{{ estimate.validity }}</td>
<td>{{ estimate.id }}</td>
<td>
{% if estimate.final_invoice %}
<a href="{% url 'cotisations:edit-custom-invoice' estimate.final_invoice.pk %}"><i style="color: #1ECA18;" class="fa fa-check"></i></a>
{% else %}
<i style="color: #D10115;" class="fa fa-times"></i>'
{% endif %}
</td>
<td>
{% can_edit estimate %}
{% include 'buttons/edit.html' with href='cotisations:edit-cost-estimate' id=estimate.id %}
{% acl_end %}
{% history_button estimate %}
{% include 'buttons/suppr.html' with href='cotisations:del-cost-estimate' id=estimate.id %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-to-invoice' estimate.id %}">
<i class="fa fa-file"></i>
</a>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-pdf' estimate.id %}">
<i class="fa fa-file-pdf-o"></i> {% trans "PDF" %}
</a>
</td>
</tr>
{% endfor %}
</table>
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
</div>

View file

@ -35,7 +35,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post">
{% csrf_token %}
{% if title %}
<h3>{{title}}</h3>
{% else %}
<h3>{% trans "Edit the invoice" %}</h3>
{% endif %}
{% massive_bootstrap_form factureform 'user' %}
{{ venteform.management_form }}
<h3>{% trans "Articles" %}</h3>

View file

@ -75,8 +75,12 @@
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
{% if fid is not None %}
{% if is_estimate %}
{\bf Devis n\textsuperscript{o} :} {{ fid }} & \\
{% else %}
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
{% endif %}
{% endif %}
\end{tabular*}
\\
@ -104,9 +108,11 @@
\begin{tabular}{|l|r|}
\hline
\textbf{Total} & {{total|floatformat:2}} \euro \\
{% if not is_estimate %}
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
\doublehline
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
{% endif %}
\hline
\end{tabular}
@ -119,6 +125,10 @@
\textbf{Remarque} & {{remark|safe}} \\
\hline
{% endif %}
{% if end_validity %}
\textbf{Validité} & Jusqu'au {{end_validity}} \\
\hline
{% endif %}
\end{tabularx}

View file

@ -0,0 +1,36 @@
{% extends "cotisations/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load acl %}
{% load i18n %}
{% block title %}{% trans "Cost estimates" %}{% endblock %}
{% block content %}
<h2>{% trans "Cost estimates list" %}</h2>
{% can_create CostEstimate %}
{% include "buttons/add.html" with href='cotisations:new-cost-estimate'%}
{% acl_end %}
{% include 'cotisations/aff_cost_estimate.html' %}
{% endblock %}

View file

@ -45,6 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
</a>
{% acl_end %}
{% can_view_all CostEstimate %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-cost-estimate" %}">
<i class="fa fa-list-ul"></i> {% trans "Cost estimate" %}
</a>
{% acl_end %}
{% can_view_all Article %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
<i class="fa fa-list-ul"></i> {% trans "Available articles" %}

View file

@ -49,8 +49,9 @@ def render_invoice(_request, ctx={}):
Render an invoice using some available information such as the current
date, the user, the articles, the prices, ...
"""
is_estimate = ctx.get('is_estimate', False)
filename = '_'.join([
'invoice',
'cost_estimate' if is_estimate else 'invoice',
slugify(ctx.get('asso_name', "")),
slugify(ctx.get('recipient_name', "")),
str(ctx.get('DATE', datetime.now()).year),

View file

@ -51,11 +51,41 @@ urlpatterns = [
views.facture_pdf,
name='facture-pdf'
),
url(
r'^new_cost_estimate/$',
views.new_cost_estimate,
name='new-cost-estimate'
),
url(
r'^index_cost_estimate/$',
views.index_cost_estimate,
name='index-cost-estimate'
),
url(
r'^cost_estimate_pdf/(?P<costestimateid>[0-9]+)$',
views.cost_estimate_pdf,
name='cost-estimate-pdf',
),
url(
r'^index_custom_invoice/$',
views.index_custom_invoice,
name='index-custom-invoice'
),
url(
r'^edit_cost_estimate/(?P<costestimateid>[0-9]+)$',
views.edit_cost_estimate,
name='edit-cost-estimate'
),
url(
r'^cost_estimate_to_invoice/(?P<costestimateid>[0-9]+)$',
views.cost_estimate_to_invoice,
name='cost-estimate-to-invoice'
),
url(
r'^del_cost_estimate/(?P<costestimateid>[0-9]+)$',
views.del_cost_estimate,
name='del-cost-estimate'
),
url(
r'^new_custom_invoice/$',
views.new_custom_invoice,

View file

@ -68,7 +68,8 @@ from .models import (
Paiement,
Banque,
CustomInvoice,
BaseInvoice
BaseInvoice,
CostEstimate
)
from .forms import (
FactureForm,
@ -81,7 +82,8 @@ from .forms import (
SelectArticleForm,
RechargeForm,
CustomInvoiceForm,
DiscountForm
DiscountForm,
CostEstimateForm,
)
from .tex import render_invoice, escape_chars
from .payment_methods.forms import payment_method_factory
@ -179,7 +181,58 @@ def new_facture(request, user, userid):
)
# TODO : change facture to invoice
@login_required
@can_create(CostEstimate)
def new_cost_estimate(request):
"""
View used to generate a custom invoice. It's mainly used to
get invoices that are not taken into account, for the administrative
point of view.
"""
# The template needs the list of articles (for the JS part)
articles = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name)
)
# Building the invocie form and the article formset
cost_estimate_form = CostEstimateForm(request.POST or None)
articles_formset = formset_factory(SelectArticleForm)(
request.POST or None,
form_kwargs={'user': request.user}
)
discount_form = DiscountForm(request.POST or None)
if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
cost_estimate_instance = cost_estimate_form.save()
for art_item in articles_formset:
if art_item.cleaned_data:
article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity']
Vente.objects.create(
facture=cost_estimate_instance,
name=article.name,
prix=article.prix,
type_cotisation=article.type_cotisation,
duration=article.duration,
number=quantity
)
discount_form.apply_to_invoice(cost_estimate_instance)
messages.success(
request,
_("The cost estimate was created.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'factureform': cost_estimate_form,
'action_name': _("Confirm"),
'articlesformset': articles_formset,
'articlelist': articles,
'discount_form': discount_form,
'title': _("Cost estimate"),
}, 'cotisations/facture.html', request)
@login_required
@can_create(CustomInvoice)
def new_custom_invoice(request):
@ -336,6 +389,55 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request)
@login_required
@can_edit(CostEstimate)
def edit_cost_estimate(request, invoice, **kwargs):
# Building the invocie form and the article formset
invoice_form = CostEstimateForm(
request.POST or None,
instance=invoice
)
purchases_objects = Vente.objects.filter(facture=invoice)
purchase_form_set = modelformset_factory(
Vente,
fields=('name', 'number'),
extra=0,
max_num=len(purchases_objects)
)
purchase_form = purchase_form_set(
request.POST or None,
queryset=purchases_objects
)
if invoice_form.is_valid() and purchase_form.is_valid():
if invoice_form.changed_data:
invoice_form.save()
purchase_form.save()
messages.success(
request,
_("The cost estimate was edited.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'factureform': invoice_form,
'venteform': purchase_form,
'title': "Edit the cost estimate"
}, 'cotisations/edit_facture.html', request)
@login_required
@can_edit(CostEstimate)
@can_create(CustomInvoice)
def cost_estimate_to_invoice(request, cost_estimate, **_kwargs):
"""Create a custom invoice from a cos estimate"""
cost_estimate.create_invoice()
messages.success(
request,
_("An invoice was successfully created from your cost estimate.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
@login_required
@can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs):
@ -371,6 +473,68 @@ def edit_custom_invoice(request, invoice, **kwargs):
}, 'cotisations/edit_facture.html', request)
@login_required
@can_view(CostEstimate)
def cost_estimate_pdf(request, invoice, **_kwargs):
"""
View used to generate a PDF file from an existing cost estimate in database
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
"""
# TODO : change vente to purchase
purchases_objects = Vente.objects.all().filter(facture=invoice)
# Get the article list and build an list out of it
# contiaining (article_name, article_price, quantity, total_price)
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
'name': escape_chars(purchase.name),
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
})
return render_invoice(request, {
'paid': invoice.paid,
'fid': invoice.id,
'DATE': invoice.date,
'recipient_name': invoice.recipient,
'address': invoice.address,
'article': purchases_info,
'total': invoice.prix_total(),
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': invoice.payment,
'remark': invoice.remark,
'end_validity': invoice.date + invoice.validity,
'is_estimate': True,
})
@login_required
@can_delete(CostEstimate)
def del_cost_estimate(request, estimate, **_kwargs):
"""
View used to delete an existing invocie.
"""
if request.method == "POST":
estimate.delete()
messages.success(
request,
_("The cost estimate was deleted.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'objet': estimate,
'objet_name': _("Cost Estimate")
}, 'cotisations/delete.html', request)
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_kwargs):
@ -412,7 +576,6 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
})
# TODO : change facture to invoice
@login_required
@can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs):
@ -763,12 +926,35 @@ def index_banque(request):
})
@login_required
@can_view_all(CustomInvoice)
def index_cost_estimate(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set')
cost_estimate_list = SortTable.sort(
cost_estimate_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_CUSTOM
)
cost_estimate_list = re2o_paginator(
request,
cost_estimate_list,
pagination_number,
)
return render(request, 'cotisations/index_cost_estimate.html', {
'cost_estimate_list': cost_estimate_list
})
@login_required
@can_view_all(CustomInvoice)
def index_custom_invoice(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids)
custom_invoice_list = SortTable.sort(
custom_invoice_list,
request.GET.get('col'),