Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

118 option wegboeken #122

Merged
merged 5 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion alexia/api/v1/methods/juliana.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
ForbiddenError, InvalidParamsError, ObjectNotFoundError,
)
from alexia.apps.billing.models import (
Authorization, Order, Product, Purchase, RfidCard,
Authorization, Order, Product, Purchase, RfidCard, WriteoffCategory, WriteOffOrder, WriteOffPurchase
)
from alexia.apps.scheduling.models import Event

Expand Down Expand Up @@ -186,3 +186,51 @@ def juliana_user_check(request, event_id, user_id):
return int(order_sum * 100)
else:
return 0

@jsonrpc_method('juliana.writeoff.save(Number,Number,Array) -> Nil', site=api_v1_site, authenticated=True)
@transaction.atomic
def juliana_writeoff_save(request, event_id, writeoff_id, purchases):
"""Saves a writeoff order in the Database"""
event = _get_validate_event(request, event_id)

try:
writeoff_cat = WriteoffCategory.objects.get(id=writeoff_id)
except WriteoffCategory.DoesNotExist:
raise InvalidParamsError('Writeoff Category %s not found' % writeoff_id)

order = WriteOffOrder(event=event, added_by=request.user, writeoff_category=writeoff_cat)
order.save()

for p in purchases:
try:
product = Product.objects.get(pk=p['product'])
except Product.DoesNotExist:
raise InvalidParamsError('Product %s not found' % p['product'])

if product.is_permanent:
product = product.permanentproduct
if product.organization != event.organizer \
or product.productgroup not in event.pricegroup.productgroups.all():
raise InvalidParamsError('Product %s is not available for this event' % p['product'])
elif product.is_temporary:
product = product.temporaryproduct
if event != product.event:
raise InvalidParamsError('Product %s is not available for this event' % p['product'])
else:
raise OtherError('Product %s is broken' % p['product'])

amount = p['amount']

if p['amount'] <= 0:
raise InvalidParamsError('Zero or negative amount not allowed')

price = amount * product.get_price(event)

if price != p['price'] / Decimal(100):
raise InvalidParamsError('Price for product %s is incorrect' % p['product'])

purchase = WriteOffPurchase(order=order, product=product.name, amount=amount, price=price)
purchase.save()

order.save(force_update=True) # ensure order.amount is correct
return True
34 changes: 33 additions & 1 deletion alexia/apps/billing/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from .models import (
Authorization, Order, PermanentProduct, PriceGroup, ProductGroup, Purchase,
RfidCard, SellingPrice,
RfidCard, SellingPrice, WriteOffOrder, WriteOffPurchase
)


Expand All @@ -18,6 +18,7 @@ class AuthorizationAdmin(admin.ModelAdmin):
class PurchaseInline(admin.TabularInline):
model = Purchase
can_delete = False
extra = 0

def get_readonly_fields(self, request, obj=None):
if obj and obj.synchronized:
Expand Down Expand Up @@ -50,6 +51,37 @@ def save_formset(self, request, form, formset, change):
form.instance.save() # Updates Order.amount


class WriteOffPurchaseInline(admin.TabularInline):
model = WriteOffPurchase
can_delete = False
extra = 0

def get_readonly_fields(self, request, obj=None):
if obj:
return self.readonly_fields + ('product', 'amount', 'price')
return self.readonly_fields


@admin.register(WriteOffOrder)
class WriteOffOrderAdmin(admin.ModelAdmin):
date_hierarchy = 'placed_at'
inlines = [WriteOffPurchaseInline]
list_display = ['event', 'placed_at', 'amount']
list_filter = ['placed_at']
raw_id_fields = ['added_by', 'event']
readonly_fields = ['placed_at']

def get_readonly_fields(self, request, obj=None):
return self.readonly_fields

def has_delete_permission(self, request, obj=None):
return False

def save_formset(self, request, form, formset, change):
formset.save()
form.instance.save() # Updates Order.amount


class SellingPriceInline(admin.TabularInline):
model = SellingPrice
extra = 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Generated by Django 2.2.28 on 2024-10-11 10:06

import alexia.core.validators
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('organization', '0025_organization_writeoff_enabled'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('scheduling', '0021_auto_20240926_1621'),
('billing', '0018_product_shortcut'),
]

operations = [
migrations.CreateModel(
name='WriteoffCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, verbose_name='name')),
('description', models.CharField(max_length=80, verbose_name='short description')),
('color', models.CharField(blank=True, max_length=6, validators=[alexia.core.validators.validate_color], verbose_name='color')),
('is_active', models.BooleanField(default=False, verbose_name='active')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organization.Organization', verbose_name='organization')),
],
),
migrations.CreateModel(
name='WriteOffOrder',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('placed_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='placed at')),
('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='amount')),
('added_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='added by')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='writeoff_orders', to='scheduling.Event', verbose_name='event')),
('writeoff_category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='billing.WriteoffCategory', verbose_name='writeoff category')),
],
options={
'verbose_name': 'writeoff order',
'verbose_name_plural': 'writeoff orders',
'ordering': ['-placed_at'],
},
),
migrations.CreateModel(
name='WriteOffPurchase',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product', models.CharField(max_length=32, verbose_name='product')),
('amount', models.PositiveSmallIntegerField(verbose_name='amount')),
('price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='price')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='writeoff_purchases', to='billing.WriteOffOrder', verbose_name='order')),
],
options={
'verbose_name': 'purchase',
'verbose_name_plural': 'purchases',
},
),
]
94 changes: 94 additions & 0 deletions alexia/apps/billing/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import unicode_literals

from collections import defaultdict
from decimal import Decimal

from django.conf import settings
Expand Down Expand Up @@ -331,3 +332,96 @@ class Meta:

def __str__(self):
return '%s x %s' % (self.amount, self.product)

@python_2_unicode_compatible
class WriteoffCategory(models.Model):
name = models.CharField(_('name'), max_length=20)
description = models.CharField(_('short description'), max_length=80)
color = models.CharField(verbose_name=_('color'), blank=True, max_length=6, validators=[validate_color])
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
verbose_name=_('organization')
)
is_active = models.BooleanField(_('active'), default=False)

def __str__(self):
return _('{name} for {organization}').format(
name=self.name,
organization=self.organization
)

@python_2_unicode_compatible
class WriteOffOrder(models.Model):
event = models.ForeignKey(Event, on_delete=models.PROTECT, related_name='writeoff_orders', verbose_name=_('event'))
placed_at = models.DateTimeField(_('placed at'), default=timezone.now)
added_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
verbose_name=_('added by'),
related_name='+',
)
amount = models.DecimalField(_('amount'), max_digits=15, decimal_places=2)

writeoff_category = models.ForeignKey(
WriteoffCategory,
on_delete=models.PROTECT, # We cannot delete purchases
verbose_name=_('writeoff category')
)

class Meta:
ordering = ['-placed_at']
verbose_name = _('writeoff order')
verbose_name_plural = _('writeoff orders')

def __str__(self):
return _('{time} on {event}').format(
time=self.placed_at.strftime('%H:%M'),
event=self.event.name,
)

def save(self, *args, **kwargs):
self.amount = self.get_price()
super(WriteOffOrder, self).save(*args, **kwargs)

def get_price(self):
amount = Decimal('0.0')
for purchase in self.writeoff_purchases.all():
amount += purchase.price
return amount


@python_2_unicode_compatible
class WriteOffPurchase(models.Model):
order = models.ForeignKey(WriteOffOrder, on_delete=models.CASCADE, related_name='writeoff_purchases', verbose_name=_('order'))
product = models.CharField(_('product'), max_length=32)
amount = models.PositiveSmallIntegerField(_('amount'))
price = models.DecimalField(_('price'), max_digits=15, decimal_places=2)

class Meta:
verbose_name = _('purchase')
verbose_name_plural = _('purchases')

def __str__(self):
return '%s x %s' % (self.amount, self.product)

@classmethod
def get_writeoff_products(cls, event):
writeoff_products = WriteOffPurchase.objects.filter(order__event=event) \
.values('order__writeoff_category__name', 'order__writeoff_category__description', 'product') \
.annotate(total_amount=models.Sum('amount'), total_price=models.Sum('price')) \
.order_by('order__writeoff_category__name', 'product')

# Group writeoffs by category
grouped_writeoff_products = defaultdict(lambda: {'products': [], 'total_amount': 0, 'total_price': 0, 'description': ''})

# calculate total product amount and total price per category
for product in writeoff_products:
category_name = product['order__writeoff_category__name']
grouped_writeoff_products[category_name]['description'] = product['order__writeoff_category__description']
grouped_writeoff_products[category_name]['products'].append(product)
grouped_writeoff_products[category_name]['total_amount'] += product['total_amount']
grouped_writeoff_products[category_name]['total_price'] += product['total_price']

# Convert defaultdict to a regular dict for easier template use
return dict(grouped_writeoff_products)
2 changes: 2 additions & 0 deletions alexia/apps/billing/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
urlpatterns = [
url(r'^order/$', views.OrderListView.as_view(), name='orders'),
url(r'^order/(?P<pk>[0-9]+)/$', views.OrderDetailView.as_view(), name='event-orders'),
url(r'^order/writeoff/(?P<pk>[0-9]+)/$', views.WriteOffExportView.as_view(), name='writeoff_export'),
url(r'^writeoff/(?P<pk>[0-9]+)/$', views.WriteOffDetailView.as_view(), name='writeoff-order'),
url(r'^order/export/$', views.OrderExportView.as_view(), name='export-orders'),
url(r'^stats/(?P<year>[0-9]{4})/$', views.OrderYearView.as_view(), name='year-orders'),
url(r'^stats/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/$', views.OrderMonthView.as_view(), name='month-orders'),
Expand Down
55 changes: 52 additions & 3 deletions alexia/apps/billing/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.core import serializers
from django.db.models import Count, Sum
from django.db.models.functions import ExtractYear, TruncMonth
from django.http import Http404, HttpResponseRedirect
from django.http import Http404, JsonResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
from django.utils.dates import MONTHS
from django.utils.translation import ugettext as _
from django.views import View
from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import (
Expand All @@ -33,8 +35,8 @@
OrganizationFilterMixin, OrganizationFormMixin,
)

from .models import Order, Purchase

from .models import Order, Purchase, WriteOffOrder, WriteOffPurchase, WriteoffCategory
import json

class JulianaView(TenderRequiredMixin, DetailView):
template_name = 'billing/juliana.html'
Expand All @@ -57,6 +59,8 @@ def get_context_data(self, **kwargs):
'products': self.get_product_list(),
'countdown': settings.JULIANA_COUNTDOWN if hasattr(settings, 'JULIANA_COUNTDOWN') else 5,
'androidapp': self.request.META.get('HTTP_X_REQUESTED_WITH') == 'net.inter_actief.juliananfc',
'writeoff': self.object.organizer.writeoff_enabled,
'writeoff_categories' : WriteoffCategory.objects.filter(organization=self.object.organizer, is_active=True)
})
return context

Expand Down Expand Up @@ -126,15 +130,60 @@ def get_context_data(self, **kwargs):
.values('product') \
.annotate(amount=Sum('amount'), price=Sum('price'))

writeoff_exists = self.object.writeoff_orders.exists

grouped_writeoff_products = None
writeoff_orders = None
if writeoff_exists:
grouped_writeoff_products = WriteOffPurchase.get_writeoff_products(event=self.object)
writeoff_orders = WriteOffOrder.objects.filter(event=self.object).order_by('-placed_at')

context = super(OrderDetailView, self).get_context_data(**kwargs)
context.update({
'orders': self.object.orders.select_related('authorization__user').order_by('-placed_at'),
'writeoff_orders': writeoff_orders,
'products': products,
'revenue': products.aggregate(Sum('price'))['price__sum'],
'writeoff_exists': writeoff_exists,
'grouped_writeoff_data': grouped_writeoff_products
})

return context


class WriteOffDetailView(LoginRequiredMixin, DetailView):
model = WriteOffOrder
template_name = 'billing/writeoff_order_detail.html'

def get_object(self, queryset=None):
obj = super(WriteOffDetailView, self).get_object(queryset)

if not self.request.user.is_superuser \
and not self.request.user.profile.is_manager(obj.authorization.organization) \
and not obj.authorization.user == self.request.user:
raise PermissionDenied

return obj


class WriteOffExportView(ManagerRequiredMixin, DenyWrongOrganizationMixin, View):
organization_field = 'organizer'

# get event
def get(self, request, *args, **kwargs):
event_pk = kwargs.get('pk') # Extract the pk from kwargs
event = get_object_or_404(Event, pk=event_pk) # Fetch the Event or raise 404 if not found

writeoff_exists = event.writeoff_orders.exists

if not writeoff_exists:
raise Http404

grouped_writeoff_products = WriteOffPurchase.get_writeoff_products(event=event)

return JsonResponse(grouped_writeoff_products)


class OrderExportView(ManagerRequiredMixin, FormView):
template_name = 'billing/order_export_form.html'
form_class = FilterEventForm
Expand Down
Loading
Loading