django-orchestra/orchestra/contrib/bills/actions.py

378 lines
14 KiB
Python
Raw Normal View History

2015-06-03 12:49:30 +00:00
import io
import zipfile
2015-05-27 14:05:25 +00:00
from datetime import date
from django.contrib import messages
2014-09-06 10:56:30 +00:00
from django.contrib.admin import helpers
2016-02-11 14:24:09 +00:00
from django.core.exceptions import ValidationError
from django.urls import reverse
2014-10-20 19:22:18 +00:00
from django.db import transaction
from django.forms.models import modelformset_factory
2016-04-06 19:00:16 +00:00
from django.http import HttpResponse, HttpResponseRedirect
2015-04-21 13:12:48 +00:00
from django.shortcuts import render, redirect
2015-07-13 11:31:32 +00:00
from django.utils import translation, timezone
2014-09-30 14:46:29 +00:00
from django.utils.safestring import mark_safe
2014-10-11 16:21:51 +00:00
from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.admin.decorators import action_with_confirmation
from orchestra.admin.forms import AdminFormSet
2014-10-11 16:21:51 +00:00
from orchestra.admin.utils import get_object_from_url, change_url
from . import settings
2014-09-06 10:56:30 +00:00
from .forms import SelectSourceForm
2016-05-20 08:29:25 +00:00
from .helpers import validate_contact, set_context_emails
2015-06-22 14:14:16 +00:00
from .models import Bill, BillLine
2014-09-30 14:46:29 +00:00
2014-08-20 18:50:07 +00:00
def view_bill(modeladmin, request, queryset):
bill = queryset.get()
2014-10-11 16:21:51 +00:00
if not validate_contact(request, bill):
return
2014-09-04 15:55:43 +00:00
html = bill.html or bill.render()
return HttpResponse(html)
view_bill.tool_description = _("View")
view_bill.url_name = 'view'
2016-05-18 14:08:12 +00:00
view_bill.hidden = True
2014-10-20 19:22:18 +00:00
@transaction.atomic
2015-07-10 13:00:51 +00:00
def close_bills(modeladmin, request, queryset, action='close_bills'):
2015-09-23 12:22:32 +00:00
# Validate bills
2014-09-30 14:46:29 +00:00
for bill in queryset:
2014-10-11 16:21:51 +00:00
if not validate_contact(request, bill):
2015-09-23 12:22:32 +00:00
return False
if not bill.is_open:
messages.warning(request, _("Selected bills should be in open state"))
return False
SelectSourceFormSet = modelformset_factory(modeladmin.model, form=SelectSourceForm, formset=AdminFormSet, extra=0)
2014-09-06 10:56:30 +00:00
formset = SelectSourceFormSet(queryset=queryset)
2014-09-08 15:10:16 +00:00
if request.POST.get('post') == 'generic_confirmation':
2014-09-06 10:56:30 +00:00
formset = SelectSourceFormSet(request.POST, request.FILES, queryset=queryset)
if formset.is_valid():
2014-10-11 16:21:51 +00:00
transactions = []
for form in formset.forms:
2014-09-06 10:56:30 +00:00
source = form.cleaned_data['source']
2014-10-11 16:21:51 +00:00
transaction = form.instance.close(payment=source)
if transaction:
transactions.append(transaction)
2014-09-16 17:14:24 +00:00
for bill in queryset:
modeladmin.log_change(request, bill, 'Closed')
messages.success(request, _("Selected bills have been closed"))
2014-10-11 16:21:51 +00:00
if transactions:
num = len(transactions)
if num == 1:
url = change_url(transactions[0])
else:
2015-05-30 14:44:05 +00:00
url = reverse('admin:payments_transaction_changelist')
url += 'id__in=%s' % ','.join([str(t.id) for t in transactions])
context = {
'url': url,
'num': num,
}
2014-10-11 16:21:51 +00:00
message = ungettext(
_('<a href="%(url)s">One related transaction</a> has been created') % context,
_('<a href="%(url)s">%(num)i related transactions</a> have been created') % context,
2014-10-11 16:21:51 +00:00
num)
messages.success(request, mark_safe(message))
return
2014-09-06 10:56:30 +00:00
opts = modeladmin.model._meta
context = {
2014-09-08 15:10:16 +00:00
'title': _("Are you sure about closing the following bills?"),
'content_message': _("Once a bill is closed it can not be further modified.</p>"
"<p>Please select a payment source for the selected bills"),
'action_name': 'Close bills',
2015-07-10 13:00:51 +00:00
'action_value': action,
2014-09-08 15:10:16 +00:00
'display_objects': [],
2014-09-06 10:56:30 +00:00
'queryset': queryset,
'opts': opts,
'app_label': opts.app_label,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'formset': formset,
2014-09-10 16:53:09 +00:00
'obj': get_object_from_url(modeladmin, request),
2014-09-06 10:56:30 +00:00
}
2016-03-04 09:46:39 +00:00
template = 'admin/orchestra/generic_confirmation.html'
if action == 'close_send_download_bills':
template = 'admin/bills/bill/close_send_download_bills.html'
return render(request, template, context)
close_bills.tool_description = _("Close")
close_bills.url_name = 'close'
2014-09-04 15:55:43 +00:00
2015-07-10 13:00:51 +00:00
def send_bills_action(modeladmin, request, queryset):
"""
raw function without confirmation
enables reuse on close_send_download_bills because of generic_confirmation.action_view
"""
2014-09-30 14:46:29 +00:00
for bill in queryset:
2014-10-11 16:21:51 +00:00
if not validate_contact(request, bill):
2015-07-09 13:04:26 +00:00
return False
2015-05-30 14:44:05 +00:00
num = 0
for bill in queryset:
2015-05-30 14:44:05 +00:00
bill.send()
2014-09-16 17:14:24 +00:00
modeladmin.log_change(request, bill, 'Sent')
2015-05-30 14:44:05 +00:00
num += 1
messages.success(request, ungettext(
_("One bill has been sent."),
_("%i bills have been sent.") % num,
num))
2015-07-10 13:00:51 +00:00
2016-05-20 08:29:25 +00:00
@action_with_confirmation(extra_context=set_context_emails)
2015-07-10 13:00:51 +00:00
def send_bills(modeladmin, request, queryset):
return send_bills_action(modeladmin, request, queryset)
2014-10-11 16:21:51 +00:00
send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send")
2014-09-04 15:55:43 +00:00
send_bills.url_name = 'send'
2015-03-29 16:10:07 +00:00
def download_bills(modeladmin, request, queryset):
for bill in queryset:
if not validate_contact(request, bill):
return False
if len(queryset) > 1:
bytesio = io.BytesIO()
archive = zipfile.ZipFile(bytesio, 'w')
for bill in queryset:
pdf = bill.as_pdf()
archive.writestr('%s.pdf' % bill.number, pdf)
archive.close()
response = HttpResponse(bytesio.getvalue(), content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"'
return response
bill = queryset[0]
pdf = bill.as_pdf()
2015-07-13 11:31:32 +00:00
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % bill.number
return response
download_bills.tool_description = _("Download")
download_bills.url_name = 'download'
def close_send_download_bills(modeladmin, request, queryset):
2015-07-10 13:00:51 +00:00
response = close_bills(modeladmin, request, queryset, action='close_send_download_bills')
2015-09-23 12:22:32 +00:00
if response is False:
# Not a valid contact or closed bill
return
if request.POST.get('post') == 'generic_confirmation':
2015-07-10 13:00:51 +00:00
response = send_bills_action(modeladmin, request, queryset)
2015-07-09 13:04:26 +00:00
if response is False:
2015-09-23 12:22:32 +00:00
# Not a valid contact
2015-07-09 13:04:26 +00:00
return
return download_bills(modeladmin, request, queryset)
2015-07-09 13:04:26 +00:00
return response
close_send_download_bills.tool_description = _("C.S.D.")
close_send_download_bills.url_name = 'close-send-download'
close_send_download_bills.help_text = _("Close, send and download bills in one shot.")
2015-04-21 13:12:48 +00:00
def manage_lines(modeladmin, request, queryset):
url = reverse('admin:bills_bill_manage_lines')
url += '?ids=%s' % ','.join(map(str, queryset.values_list('id', flat=True)))
return redirect(url)
@action_with_confirmation()
2015-03-29 16:10:07 +00:00
def undo_billing(modeladmin, request, queryset):
group = {}
for line in queryset.select_related('order'):
if line.order_id:
try:
group[line.order].append(line)
except KeyError:
group[line.order] = [line]
2015-05-27 14:05:25 +00:00
# Validate
2015-04-02 16:14:55 +00:00
for order, lines in group.items():
2015-05-27 14:05:25 +00:00
prev = None
billed_on = date.max
billed_until = date.max
for line in sorted(lines, key=lambda l: l.start_on):
if billed_on is not None:
if line.order_billed_on is None:
billed_on = line.order_billed_on
else:
billed_on = min(billed_on, line.order_billed_on)
if billed_until is not None:
if line.order_billed_until is None:
billed_until = line.order_billed_until
else:
billed_until = min(billed_until, line.order_billed_until)
if prev:
if line.start_on != prev:
messages.error(request, "Line dates doesn't match.")
return
else:
# First iteration
if order.billed_on < line.start_on:
messages.error(request, "Billed on is smaller than first line start_on.")
2015-05-27 14:05:25 +00:00
return
prev = line.end_on
nlines += 1
if not prev:
messages.error(request, "Order does not have lines!.")
order.billed_until = billed_until
order.billed_on = billed_on
2015-05-27 14:05:25 +00:00
# Commit changes
norders, nlines = 0, 0
for order, lines in group.items():
for line in lines:
nlines += 1
line.delete()
# TODO update order history undo billing
2015-05-27 14:05:25 +00:00
order.save(update_fields=('billed_until', 'billed_on'))
norders += 1
2015-05-27 14:05:25 +00:00
messages.success(request, _("%(norders)s orders and %(nlines)s lines undoed.") % {
'nlines': nlines,
'norders': norders
})
2015-03-29 16:10:07 +00:00
def move_lines(modeladmin, request, queryset, action=None):
2015-03-29 16:10:07 +00:00
# Validate
target = request.GET.get('target')
if not target:
# select target
2015-04-21 13:12:48 +00:00
context = {}
2015-03-29 16:10:07 +00:00
return render(request, 'admin/orchestra/generic_confirmation.html', context)
target = Bill.objects.get(pk=int(pk))
if request.POST.get('post') == 'generic_confirmation':
for line in queryset:
line.bill = target
line.save(update_fields=['bill'])
# TODO bill history update
messages.success(request, _("Lines moved"))
# Final confirmation
return render(request, 'admin/orchestra/generic_confirmation.html', context)
def copy_lines(modeladmin, request, queryset):
# same as move, but changing action behaviour
return move_lines(modeladmin, request, queryset)
2015-06-22 14:14:16 +00:00
2016-02-11 14:24:09 +00:00
def validate_amend_bills(bills):
for bill in bills:
if bill.is_open:
raise ValidationError(_("Selected bills should be in closed state"))
if bill.type not in bill.AMEND_MAP:
raise ValidationError(_("%s can not be amended.") % bill.get_type_display())
@action_with_confirmation(validator=validate_amend_bills)
2015-06-22 14:14:16 +00:00
def amend_bills(modeladmin, request, queryset):
amend_ids = []
2015-06-22 14:14:16 +00:00
for bill in queryset:
with translation.override(bill.account.language):
amend_type = bill.get_amend_type()
context = {
'related_type': _(bill.get_type_display()),
'number': bill.number,
'date': bill.created_on,
}
amend = Bill.objects.create(
account=bill.account,
2015-07-02 10:49:44 +00:00
type=amend_type,
amend_of=bill,
2015-06-22 14:14:16 +00:00
)
context['type'] = _(amend.get_type_display())
amend.comments = _("%(type)s of %(related_type)s %(number)s and creation date %(date)s") % context
amend.save(update_fields=('comments',))
for tax, subtotals in bill.compute_subtotals().items():
context['tax'] = tax
line = BillLine.objects.create(
bill=amend,
start_on=bill.created_on,
description=_("%(related_type)s %(number)s subtotal for tax %(tax)s%%") % context,
2015-06-22 14:14:16 +00:00
subtotal=subtotals[0],
tax=tax
)
amend_ids.append(amend.pk)
2016-04-27 08:35:13 +00:00
modeladmin.log_change(request, bill, 'Amended, amend id is %i' % amend.id)
num = len(amend_ids)
if num == 1:
amend_url = reverse('admin:bills_bill_change', args=amend_ids)
else:
amend_url = reverse('admin:bills_bill_changelist')
amend_url += '?id=%s' % ','.join(map(str, amend_ids))
context = {
'url': amend_url,
'num': num,
}
2015-06-22 14:14:16 +00:00
messages.success(request, mark_safe(ungettext(
_('<a href="%(url)s">One amendment bill</a> have been generated.') % context,
_('<a href="%(url)s">%(num)i amendment bills</a> have been generated.') % context,
num
2015-06-22 14:14:16 +00:00
)))
amend_bills.tool_description = _("Amend")
2015-06-22 14:14:16 +00:00
amend_bills.url_name = 'amend'
2015-07-13 11:31:32 +00:00
def bill_report(modeladmin, request, queryset):
2015-07-10 13:00:51 +00:00
subtotals = {}
total = 0
for bill in queryset:
for tax, subtotal in bill.compute_subtotals().items():
try:
subtotals[tax][0] += subtotal[0]
except KeyError:
subtotals[tax] = subtotal
else:
subtotals[tax][1] += subtotal[1]
2015-07-13 11:31:32 +00:00
total += bill.compute_total()
context = {
2015-07-10 13:00:51 +00:00
'subtotals': subtotals,
'total': total,
'bills': queryset,
'currency': settings.BILLS_CURRENCY,
}
2015-07-13 11:31:32 +00:00
return render(request, 'admin/bills/bill/report.html', context)
def service_report(modeladmin, request, queryset):
services = {}
totals = [0, 0, 0, 0, 0]
now = timezone.now().date()
if queryset.model == Bill:
queryset = BillLine.objects.filter(bill_id__in=queryset.values_list('id', flat=True))
# Filter amends
queryset = queryset.filter(bill__amend_of__isnull=True)
for line in queryset.select_related('order__service').prefetch_related('sublines'):
order, service = None, None
if line.order_id:
order = line.order
service = order.service
name = service.description
active, cancelled = (1, 0) if not order.cancelled_on or order.cancelled_on > now else (0, 1)
nominal_price = order.service.nominal_price
else:
name = '*%s' % line.description
active = 1
cancelled = 0
nominal_price = 0
try:
info = services[name]
except KeyError:
info = [active, cancelled, nominal_price, line.quantity or 1, line.compute_total()]
services[name] = info
else:
info[0] += active
info[1] += cancelled
info[3] += line.quantity or 1
info[4] += line.compute_total()
totals[0] += active
totals[1] += cancelled
totals[2] += nominal_price
totals[3] += line.quantity or 1
totals[4] += line.compute_total()
context = {
'services': sorted(services.items(), key=lambda n: -n[1][4]),
'totals': totals,
}
return render(request, 'admin/bills/billline/report.html', context)
2016-04-06 19:00:16 +00:00
2016-04-07 11:14:44 +00:00
def list_bills(modeladmin, request, queryset):
ids = ','.join(map(str, queryset.values_list('bill_id', flat=True).distinct()))
return HttpResponseRedirect('../bill/?id__in=%s' % ids)