Comment calculer le total par mois sans utiliser de supplément?
J'utilise actuellement:
Exemple.
Ce que j'ai essayé jusqu'à présent.
#Doesn't work for me but I don't mind because I don't want to use extra
truncate_month = connection.ops.date_trunc_sql('month','day')
invoices = Invoice.objects.filter(is_deleted = False,company = company).extra({'month': truncate_month}).values('month').annotate(Sum('total'))
----
#It works but I think that it's too slow if I query a big set of data
for current_month in range(1,13):
Invoice.objects.filter(date__month = current__month).annotate(total = Sum("total"))
et aussi celui-ci, la réponse semble excellente mais je ne peux pas importer le module TruncMonth.
Django: Grouper par date (jour, mois, année)
P.S. Je sais que cette question est déjà posée plusieurs fois mais je ne vois aucune réponse.
Merci!
[~ # ~] solution [~ # ~] :
Merci à la réponse de @ Vin-G.
Tout d'abord, vous devez créer une fonction qui peut extraire le mois pour vous:
from Django.db import models
from Django.db.models import Func
class Month(Func):
function = 'EXTRACT'
template = '%(function)s(MONTH from %(expressions)s)'
output_field = models.IntegerField()
Après cela, tout ce que vous devez faire est
values()
Sum()
Important : si votre classe de modèle a un ordre par défaut spécifié dans les méta-options, alors vous devrez ajouter une clause order_by()
vide . C'est à cause de https://docs.djangoproject.com/en/1.9/topics/db/aggregation/#interaction-with-default-ordering-or-order-by
Les champs qui sont mentionnés dans la partie
order_by()
d'un ensemble de requêtes (ou qui sont utilisés dans l'ordre par défaut sur un modèle) sont utilisés lors de la sélection des données de sortie, même s'ils ne sont pas autrement spécifiés dans la fonctionvalues()
appel. Ces champs supplémentaires sont utilisés pour regrouper les résultats "similaires" et ils peuvent faire apparaître des lignes de résultats autrement identiques comme étant distinctes.
Si vous n'êtes pas sûr, vous pouvez simplement ajouter la clause order_by()
vide de toute façon sans aucun effet négatif.
c'est à dire.
from Django.db.models import Sum
summary = (Invoice.objects
.annotate(m=Month('date'))
.values('m')
.annotate(total=Sum('total'))
.order_by())
Voir l'intégralité de Gist ici: https://Gist.github.com/alvingonzales/ff9333e39d221981e5fc4cd6cdafdd17
Si vous avez besoin de plus d'informations:
Détails sur la création de vos propres classes Func: https://docs.djangoproject.com/en/1.8/ref/models/expressions/#func-expressions
Détails sur la clause values (), (faites attention à la façon dont elle interagit avec annotate () par rapport à l'ordre des clauses): https://docs.djangoproject.com/en/1.9/topics/db/ agrégation/# valeurs
l'ordre dans lequel les clauses annotate () et values () sont appliquées à une requête est significatif. Si la clause values () précède l'annotate (), l'annotation sera calculée en utilisant le regroupement décrit par la clause values ().
itertools.groupby
est l'option performante de Python et peut être utilisée avec une seule requête db:
from itertools import groupby
invoices = Invoice.objects.only('date', 'total').order_by('date')
month_totals = {
k: sum(x.total for x in g)
for k, g in groupby(invoices, key=lambda i: i.date.month)
}
month_totals
# {1: 100, 3: 100, 4: 500, 7: 500}
Je ne connais pas de pure solution Django ORM. Le date__month
le filtre est très limité et ne peut pas être utilisé dans values
, order_by
, etc.
Je ne sais pas si ma solution est plus rapide que la vôtre. Vous devez le profiler. Néanmoins, je ne demande la base de données qu'une seule fois au lieu de 12 fois.
#utils.py
from Django.db.models import Count, Sum
def get_total_per_month_value():
"""
Return the total of sales per month
ReturnType: [Dict]
{'December': 3400, 'February': 224, 'January': 792}
"""
result= {}
db_result = Sale.objects.values('price','created')
for i in db_result:
month = str(i.get('created').strftime("%B"))
if month in result.keys():
result[month] = result[month] + i.get('price')
else:
result[month] = i.get('price')
return result
#models.py
class Sale(models.Model):
price = models.PositiveSmallIntegerField()
created = models.DateTimeField(_(u'Published'), default="2001-02-24")
#views.py
from .utils import get_total_per_month_value
# ...
result = get_total_per_month_value()
test.py
#
import pytest
from mixer.backend.Django import mixer
#Don't try to write in the database
pytestmark = pytest.mark.Django_db
def test_get_total_per_month():
from .utils import get_total_per_month_value
selected_date = ['01','02','03','01','01']
#2016-01-12 == YYYY-MM-DD
for i in selected_date:
mixer.blend('myapp.Sale', created="2016-"+i+"-12")
values = get_total_per_month_value() #return a dict
months = values.keys()
assert 'January' in months, 'Should include January'
assert 'February' in months, 'Should include February'
assert len(months) == 3, 'Should aggregate the months'
result = (
invoices.objects
.all()
.values_list('created_at__year', 'created_at__month')
.annotate(Sum('total'))
.order_by('created_at__year', 'created_at__month')
)
N'oubliez pas que Django fournissent un gestionnaire natif datetimes
, qui vous permet de retirer facilement tous les jours/semaines/mois/années) de tout ensemble de requêtes pour les modèles avec un champ datetime. Donc, si le modèle Invoice
ci-dessus a un champ datetime created
, et que vous voulez des totaux pour chaque mois dans votre ensemble de requêtes, vous pouvez simplement faire:
invoices = Invoice.objects.all()
months = invoices.datetimes("created", kind="month")
for month in months:
month_invs = invoices.filter(created__month=month.month)
month_total = month_invs.aggregate(total=Sum("otherfield")).get("total")
print(f"Month: {month}, Total: {month_total}")
Pas de fonctions externes ou deps nécessaires.