2015-06-03 12:49:30 +00:00
|
|
|
import io
|
2014-09-03 22:01:44 +00:00
|
|
|
import zipfile
|
2015-05-27 14:05:25 +00:00
|
|
|
from datetime import date
|
2014-09-03 22:01:44 +00:00
|
|
|
|
2014-09-05 14:27:30 +00:00
|
|
|
from django.contrib import messages
|
2014-09-06 10:56:30 +00:00
|
|
|
from django.contrib.admin import helpers
|
2015-04-04 17:44:07 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2014-09-30 14:46:29 +00:00
|
|
|
from django.core.urlresolvers import reverse
|
2014-10-20 19:22:18 +00:00
|
|
|
from django.db import transaction
|
2014-10-11 16:21:51 +00:00
|
|
|
from django.http import HttpResponse
|
2015-04-21 13:12:48 +00:00
|
|
|
from django.shortcuts import render, redirect
|
2015-06-22 14:14:16 +00:00
|
|
|
from django.utils import translation
|
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 _
|
2014-09-03 22:01:44 +00:00
|
|
|
|
2014-09-06 10:56:30 +00:00
|
|
|
from orchestra.admin.forms import adminmodelformset_factory
|
2015-07-09 10:19:30 +00:00
|
|
|
from orchestra.admin.decorators import action_with_confirmation
|
2014-10-11 16:21:51 +00:00
|
|
|
from orchestra.admin.utils import get_object_from_url, change_url
|
2014-09-03 22:01:44 +00:00
|
|
|
from orchestra.utils.html import html_to_pdf
|
|
|
|
|
2015-07-07 10:41:34 +00:00
|
|
|
from . import settings
|
2014-09-06 10:56:30 +00:00
|
|
|
from .forms import SelectSourceForm
|
2014-10-11 16:21:51 +00:00
|
|
|
from .helpers import validate_contact
|
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
|
|
|
|
2014-09-03 22:01:44 +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)
|
2014-09-03 22:01:44 +00:00
|
|
|
view_bill.verbose_name = _("View")
|
|
|
|
view_bill.url_name = 'view'
|
|
|
|
|
|
|
|
|
2014-10-20 19:22:18 +00:00
|
|
|
@transaction.atomic
|
2014-09-03 22:01:44 +00:00
|
|
|
def close_bills(modeladmin, request, queryset):
|
2014-09-18 15:07:39 +00:00
|
|
|
queryset = queryset.filter(is_open=True)
|
2014-09-05 14:27:30 +00:00
|
|
|
if not queryset:
|
|
|
|
messages.warning(request, _("Selected bills should be in open state"))
|
|
|
|
return
|
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):
|
|
|
|
return
|
2015-04-27 12:24:17 +00:00
|
|
|
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, 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)
|
2014-09-05 14:27:30 +00:00
|
|
|
if formset.is_valid():
|
2014-10-11 16:21:51 +00:00
|
|
|
transactions = []
|
2014-09-05 14:27:30 +00:00
|
|
|
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')
|
2014-09-05 14:27:30 +00:00
|
|
|
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])
|
2015-05-28 09:43:57 +00:00
|
|
|
context = {
|
|
|
|
'url': url,
|
|
|
|
'num': num,
|
|
|
|
}
|
2014-10-11 16:21:51 +00:00
|
|
|
message = ungettext(
|
2015-05-28 09:43:57 +00:00
|
|
|
_('<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))
|
2014-09-05 14:27:30 +00:00
|
|
|
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',
|
2014-09-06 10:56:30 +00:00
|
|
|
'action_value': 'close_bills',
|
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
|
|
|
}
|
2014-09-08 15:10:16 +00:00
|
|
|
return render(request, 'admin/orchestra/generic_confirmation.html', context)
|
2014-09-03 22:01:44 +00:00
|
|
|
close_bills.verbose_name = _("Close")
|
|
|
|
close_bills.url_name = 'close'
|
2014-09-04 15:55:43 +00:00
|
|
|
|
|
|
|
|
2015-07-09 10:19:30 +00:00
|
|
|
@action_with_confirmation()
|
2014-09-04 15:55:43 +00:00
|
|
|
def send_bills(modeladmin, request, queryset):
|
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):
|
|
|
|
return
|
2015-05-30 14:44:05 +00:00
|
|
|
num = 0
|
2015-05-28 09:43:57 +00:00
|
|
|
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(
|
2015-05-28 09:43:57 +00:00
|
|
|
_("One bill has been sent."),
|
|
|
|
_("%i bills have been sent.") % num,
|
|
|
|
num))
|
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
|
|
|
|
|
|
|
|
2015-07-09 10:19:30 +00:00
|
|
|
def download_bills(modeladmin, request, queryset):
|
|
|
|
if queryset.count() > 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.get()
|
|
|
|
pdf = bill.as_pdf()
|
|
|
|
return HttpResponse(pdf, content_type='application/pdf')
|
|
|
|
download_bills.verbose_name = _("Download")
|
|
|
|
download_bills.url_name = 'download'
|
|
|
|
|
|
|
|
|
|
|
|
def close_send_download_bills(modeladmin, request, queryset):
|
|
|
|
close_bills(modeladmin, request, queryset)
|
|
|
|
if request.POST.get('post') == 'generic_confirmation':
|
|
|
|
send_bills(modeladmin, request, queryset)
|
|
|
|
return download_bills(modeladmin, request, queryset)
|
|
|
|
close_send_download_bills.verbose_name = _("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)
|
|
|
|
|
|
|
|
|
2015-07-09 10:19:30 +00:00
|
|
|
@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:
|
2015-05-28 09:43:57 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
# Commit changes
|
|
|
|
norders, nlines = 0, 0
|
|
|
|
for order, lines in group.items():
|
|
|
|
for line in lines:
|
|
|
|
nlines += 1
|
|
|
|
line.delete()
|
2015-05-28 09:43:57 +00:00
|
|
|
# TODO update order history undo billing
|
2015-05-27 14:05:25 +00:00
|
|
|
order.save(update_fields=('billed_until', 'billed_on'))
|
|
|
|
norders += 1
|
|
|
|
|
|
|
|
messages.success(request, _("%(norders)s orders and %(nlines)s lines undoed.") % {
|
|
|
|
'nlines': nlines,
|
|
|
|
'norders': norders
|
|
|
|
})
|
|
|
|
|
2015-03-29 16:10:07 +00:00
|
|
|
|
2015-05-28 09:43:57 +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
|
2015-05-28 09:43:57 +00:00
|
|
|
return move_lines(modeladmin, request, queryset)
|
2015-06-22 14:14:16 +00:00
|
|
|
|
|
|
|
|
2015-07-09 10:19:30 +00:00
|
|
|
@action_with_confirmation()
|
2015-06-22 14:14:16 +00:00
|
|
|
def amend_bills(modeladmin, request, queryset):
|
|
|
|
if queryset.filter(is_open=True).exists():
|
|
|
|
messages.warning(request, _("Selected bills should be in closed state"))
|
|
|
|
return
|
2015-07-07 10:41:34 +00:00
|
|
|
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,
|
2015-07-07 10:41:34 +00:00
|
|
|
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
|
|
|
|
)
|
2015-07-07 10:41:34 +00:00
|
|
|
amend_ids.append(amend.pk)
|
|
|
|
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(
|
2015-07-07 10:41:34 +00:00
|
|
|
_('<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.verbose_name = _("Amend")
|
|
|
|
amend_bills.url_name = 'amend'
|
2015-07-07 10:41:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
def report(modeladmin, request, queryset):
|
|
|
|
context = {
|
|
|
|
'bills': queryset,
|
|
|
|
'currency': settings.BILLS_CURRENCY,
|
|
|
|
}
|
|
|
|
return render(request, 'admin/bills/report.html', context)
|