Merge branch 'chrisjrn/better_reports'
This commit is contained in:
		
						commit
						4ad96286c3
					
				
					 3 changed files with 220 additions and 94 deletions
				
			
		|  | @ -4,6 +4,7 @@ from django import forms | ||||||
| 
 | 
 | ||||||
| # Staff-facing forms. | # Staff-facing forms. | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class ProductAndCategoryForm(forms.Form): | class ProductAndCategoryForm(forms.Form): | ||||||
|     product = forms.ModelMultipleChoiceField( |     product = forms.ModelMultipleChoiceField( | ||||||
|         queryset=inventory.Product.objects.all(), |         queryset=inventory.Product.objects.all(), | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| from django.contrib.auth.decorators import user_passes_test | from django.contrib.auth.decorators import user_passes_test | ||||||
| from django.shortcuts import render | from django.shortcuts import render | ||||||
|  | from django.core.urlresolvers import reverse | ||||||
| from functools import wraps | from functools import wraps | ||||||
| 
 | 
 | ||||||
| from registrasion import views | from registrasion import views | ||||||
|  | @ -12,34 +13,172 @@ _all_report_views = [] | ||||||
| 
 | 
 | ||||||
| class Report(object): | class Report(object): | ||||||
| 
 | 
 | ||||||
|     def __init__(self, title, headings, data, link_view=None): |     def __init__(self): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def title(): | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     def headings(): | ||||||
|  |         ''' Returns the headings for the report. ''' | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     def rows(content_type): | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         Arguments: | ||||||
|  |             content_type (str): The content-type for the output format of this | ||||||
|  |             report. | ||||||
|  | 
 | ||||||
|  |         Returns: | ||||||
|  |             An iterator, which yields each row of the data. Each row should | ||||||
|  |             be an iterable containing the cells, rendered appropriately for | ||||||
|  |             content_type. | ||||||
|  |         ''' | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     def _linked_text(self, content_type, address, text): | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         Returns: | ||||||
|  |             an HTML linked version of text, if the content_type for this report | ||||||
|  |             is HTMLish, otherwise, the text. | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         if content_type == "text/html": | ||||||
|  |             return Report._html_link(address, text) | ||||||
|  |         else: | ||||||
|  |             return text | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def _html_link(address, text): | ||||||
|  |         return '<a href="%s">%s</a>' % (address, text) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _ReportTemplateWrapper(object): | ||||||
|  |     ''' Used internally to pass `Report` objects to templates. They effectively | ||||||
|  |     are used to specify the content_type for a report. ''' | ||||||
|  | 
 | ||||||
|  |     def __init__(self, content_type, report): | ||||||
|  |         self.content_type = content_type | ||||||
|  |         self.report = report | ||||||
|  | 
 | ||||||
|  |     def title(self): | ||||||
|  |         return self.report.title() | ||||||
|  | 
 | ||||||
|  |     def headings(self): | ||||||
|  |         return self.report.headings() | ||||||
|  | 
 | ||||||
|  |     def rows(self): | ||||||
|  |         return self.report.rows(self.content_type) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BasicReport(Report): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, title, headings, link_view=None): | ||||||
|  |         super(BasicReport, self).__init__() | ||||||
|         self._title = title |         self._title = title | ||||||
|         self._headings = headings |         self._headings = headings | ||||||
|         self._data = data |  | ||||||
|         self._link_view = link_view |         self._link_view = link_view | ||||||
| 
 | 
 | ||||||
|     @property |  | ||||||
|     def title(self): |     def title(self): | ||||||
|         ''' Returns the title for this report. ''' |         ''' Returns the title for this report. ''' | ||||||
|         return self._title |         return self._title | ||||||
| 
 | 
 | ||||||
|     @property |  | ||||||
|     def headings(self): |     def headings(self): | ||||||
|         ''' Returns the headings for the table. ''' |         ''' Returns the headings for the table. ''' | ||||||
|         return self._headings |         return self._headings | ||||||
| 
 | 
 | ||||||
|     @property |     def cell_text(self, content_type, index, text): | ||||||
|     def data(self): |         if index > 0 or not self._link_view: | ||||||
|  |             return text | ||||||
|  |         else: | ||||||
|  |             address = self.get_link(text) | ||||||
|  |             return self._linked_text(content_type, address, text) | ||||||
|  | 
 | ||||||
|  |     def get_link(self, argument): | ||||||
|  |         return reverse(self._link_view, args=[argument]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ListReport(BasicReport): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, title, headings, data, link_view=None): | ||||||
|  |         super(ListReport, self).__init__(title, headings, link_view=link_view) | ||||||
|  |         self._data = data | ||||||
|  | 
 | ||||||
|  |     def rows(self, content_type): | ||||||
|         ''' Returns the data rows for the table. ''' |         ''' Returns the data rows for the table. ''' | ||||||
|         return self._data |  | ||||||
| 
 | 
 | ||||||
|     @property |         for row in self._data: | ||||||
|     def link_view(self): |             yield [ | ||||||
|         ''' Returns the URL name or the view callable that can be used to |                 self.cell_text(content_type, i, cell) | ||||||
|         view the row's detail. The left-most value is passed into `reverse` |                 for i, cell in enumerate(row) | ||||||
|         as an argument. ''' |             ] | ||||||
| 
 | 
 | ||||||
|         return self._link_view | 
 | ||||||
|  | class QuerysetReport(BasicReport): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, title, attributes, queryset, headings=None, | ||||||
|  |                  link_view=None): | ||||||
|  |         super(QuerysetReport, self).__init__( | ||||||
|  |             title, headings, link_view=link_view | ||||||
|  |         ) | ||||||
|  |         self._attributes = attributes | ||||||
|  |         self._queryset = queryset | ||||||
|  | 
 | ||||||
|  |     def headings(self): | ||||||
|  |         if self._headings is not None: | ||||||
|  |             return self._headings | ||||||
|  | 
 | ||||||
|  |         return [ | ||||||
|  |             " ".join(i.split("_")).capitalize() for i in self._attributes | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |     def rows(self, content_type): | ||||||
|  | 
 | ||||||
|  |         def rgetattr(item, attr): | ||||||
|  |             for i in attr.split("__"): | ||||||
|  |                 item = getattr(item, i) | ||||||
|  | 
 | ||||||
|  |             if callable(item): | ||||||
|  |                 try: | ||||||
|  |                     return item() | ||||||
|  |                 except TypeError: | ||||||
|  |                     pass | ||||||
|  | 
 | ||||||
|  |             return item | ||||||
|  | 
 | ||||||
|  |         for row in self._queryset: | ||||||
|  |             yield [ | ||||||
|  |                 self.cell_text(content_type, i, rgetattr(row, attribute)) | ||||||
|  |                 for i, attribute in enumerate(self._attributes) | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Links(Report): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, title, links): | ||||||
|  |         ''' | ||||||
|  |         Arguments: | ||||||
|  |             links ([tuple, ...]): a list of 2-tuples: | ||||||
|  |                 (url, link_text) | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         self._title = title | ||||||
|  |         self._links = links | ||||||
|  | 
 | ||||||
|  |     def title(self): | ||||||
|  |         return self._title | ||||||
|  | 
 | ||||||
|  |     def headings(self): | ||||||
|  |         return [] | ||||||
|  | 
 | ||||||
|  |     def rows(self, content_type): | ||||||
|  |         print self._links | ||||||
|  |         for url, link_text in self._links: | ||||||
|  |             yield [ | ||||||
|  |                 self._linked_text(content_type, url, link_text) | ||||||
|  |             ] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def report_view(title, form_type=None): | def report_view(title, form_type=None): | ||||||
|  | @ -72,6 +211,11 @@ def report_view(title, form_type=None): | ||||||
|             if isinstance(reports, Report): |             if isinstance(reports, Report): | ||||||
|                 reports = [reports] |                 reports = [reports] | ||||||
| 
 | 
 | ||||||
|  |             reports = [ | ||||||
|  |                 _ReportTemplateWrapper("text/html", report) | ||||||
|  |                 for report in reports | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|             ctx = { |             ctx = { | ||||||
|                 "title": title, |                 "title": title, | ||||||
|                 "form": form, |                 "form": form, | ||||||
|  |  | ||||||
|  | @ -14,7 +14,9 @@ from registrasion.models import people | ||||||
| from registrasion import views | from registrasion import views | ||||||
| 
 | 
 | ||||||
| from reports import get_all_reports | from reports import get_all_reports | ||||||
| from reports import Report | from reports import Links | ||||||
|  | from reports import ListReport | ||||||
|  | from reports import QuerysetReport | ||||||
| from reports import report_view | from reports import report_view | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -90,7 +92,7 @@ def items_sold(request, form): | ||||||
|         "(TOTAL)", "--", "--", total_income, |         "(TOTAL)", "--", "--", total_income, | ||||||
|     ]) |     ]) | ||||||
| 
 | 
 | ||||||
|     return Report("Paid items", headings, data) |     return ListReport("Paid items", headings, data) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @report_view("Reconcilitation") | @report_view("Reconcilitation") | ||||||
|  | @ -128,7 +130,7 @@ def reconciliation(request, form): | ||||||
|         sales["total"] - payments["total"] - ucn["total"], |         sales["total"] - payments["total"] - ucn["total"], | ||||||
|     ]) |     ]) | ||||||
| 
 | 
 | ||||||
|     return Report("Sales and Payments", headings, data) |     return ListReport("Sales and Payments", headings, data) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @report_view("Product status", form_type=forms.ProductAndCategoryForm) | @report_view("Product status", form_type=forms.ProductAndCategoryForm) | ||||||
|  | @ -211,7 +213,7 @@ def product_status(request, form): | ||||||
|             item["total_refunded"], |             item["total_refunded"], | ||||||
|         ]) |         ]) | ||||||
| 
 | 
 | ||||||
|     return Report("Inventory", headings, data) |     return ListReport("Inventory", headings, data) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @report_view("Credit notes") | @report_view("Credit notes") | ||||||
|  | @ -225,20 +227,13 @@ def credit_notes(request, form): | ||||||
|         "invoice__user__attendee__attendeeprofilebase", |         "invoice__user__attendee__attendeeprofilebase", | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     headings = [ |     return QuerysetReport( | ||||||
|         "id", "Owner", "Status", "Value", |         "Credit Notes", | ||||||
|     ] |         ["id", "invoice__user__attendee__attendeeprofilebase__invoice_recipient", "status", "value"],  # NOQA | ||||||
| 
 |         notes, | ||||||
|     data = [] |         headings=["id", "Owner", "Status", "Value"], | ||||||
|     for note in notes: |         link_view=views.credit_note, | ||||||
|         data.append([ |     ) | ||||||
|             note.id, |  | ||||||
|             note.invoice.user.attendee.attendeeprofilebase.invoice_recipient(), |  | ||||||
|             note.status, |  | ||||||
|             note.value, |  | ||||||
|         ]) |  | ||||||
| 
 |  | ||||||
|     return Report("Credit Notes", headings, data, link_view="credit_note") |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @report_view("Attendee", form_type=forms.UserIdForm) | @report_view("Attendee", form_type=forms.UserIdForm) | ||||||
|  | @ -253,82 +248,63 @@ def attendee(request, form, user_id=None): | ||||||
|         user_id = form.cleaned_data["user"] |         user_id = form.cleaned_data["user"] | ||||||
| 
 | 
 | ||||||
|     attendee = people.Attendee.objects.get(user__id=user_id) |     attendee = people.Attendee.objects.get(user__id=user_id) | ||||||
|  |     name = attendee.attendeeprofilebase.attendee_name() | ||||||
| 
 | 
 | ||||||
|     reports = [] |     reports = [] | ||||||
| 
 | 
 | ||||||
|     # TODO: METADATA. |     links = [] | ||||||
|  |     links.append(( | ||||||
|  |         reverse(views.amend_registration, args=[user_id]), | ||||||
|  |         "Amend current cart", | ||||||
|  |     )) | ||||||
|  |     reports.append(Links("Actions for " + name, links)) | ||||||
| 
 | 
 | ||||||
|  |     # Paid and pending  products | ||||||
|     ic = ItemController(attendee.user) |     ic = ItemController(attendee.user) | ||||||
|     # Paid products |     reports.append(ListReport( | ||||||
|     headings = ["Product", "Quantity"] |         "Paid Products", | ||||||
|     data = [] |         ["Product", "Quantity"], | ||||||
| 
 |         [(pq.product, pq.quantity) for pq in ic.items_purchased()], | ||||||
|     for pq in ic.items_purchased(): |     )) | ||||||
|         data.append([ |     reports.append(ListReport( | ||||||
|             pq.product, |         "Unpaid Products", | ||||||
|             pq.quantity, |         ["Product", "Quantity"], | ||||||
|         ]) |         [(pq.product, pq.quantity) for pq in ic.items_pending()], | ||||||
| 
 |     )) | ||||||
|     reports.append(Report("Paid Products", headings, data)) |  | ||||||
| 
 |  | ||||||
|     # Unpaid products |  | ||||||
|     headings = ["Product", "Quantity"] |  | ||||||
|     data = [] |  | ||||||
| 
 |  | ||||||
|     for pq in ic.items_pending(): |  | ||||||
|         data.append([ |  | ||||||
|             pq.product, |  | ||||||
|             pq.quantity, |  | ||||||
|         ]) |  | ||||||
| 
 |  | ||||||
|     reports.append( Report("Unpaid Products", headings, data)) |  | ||||||
| 
 | 
 | ||||||
|     # Invoices |     # Invoices | ||||||
|     headings = ["Invoice ID", "Status", "Value"] |  | ||||||
|     data = [] |  | ||||||
| 
 |  | ||||||
|     invoices = commerce.Invoice.objects.filter( |     invoices = commerce.Invoice.objects.filter( | ||||||
|         user=attendee.user, |         user=attendee.user, | ||||||
|     ) |     ) | ||||||
|     for invoice in invoices: |     reports.append(QuerysetReport( | ||||||
|         data.append([ |         "Invoices", | ||||||
|             invoice.id, invoice.get_status_display(), invoice.value, |         ["id", "get_status_display", "value"], | ||||||
|         ]) |         invoices, | ||||||
| 
 |         headings=["Invoice ID", "Status", "Value"], | ||||||
|     reports.append(Report("Invoices", headings, data, link_view="invoice")) |         link_view=views.invoice, | ||||||
|  |     )) | ||||||
| 
 | 
 | ||||||
|     # Credit Notes |     # Credit Notes | ||||||
|     headings = ["Note ID", "Status", "Value"] |  | ||||||
|     data = [] |  | ||||||
| 
 |  | ||||||
|     credit_notes = commerce.CreditNote.objects.filter( |     credit_notes = commerce.CreditNote.objects.filter( | ||||||
|         invoice__user=attendee.user, |         invoice__user=attendee.user, | ||||||
|     ) |     ) | ||||||
|     for credit_note in credit_notes: |     reports.append(QuerysetReport( | ||||||
|         data.append([ |         "Credit Notes", | ||||||
|             credit_note.id, credit_note.status, credit_note.value, |         ["id", "status", "value"], | ||||||
|         ]) |         credit_notes, | ||||||
| 
 |         link_view=views.credit_note, | ||||||
|     reports.append( |     )) | ||||||
|         Report("Credit Notes", headings, data, link_view="credit_note") |  | ||||||
|     ) |  | ||||||
| 
 | 
 | ||||||
|     # All payments |     # All payments | ||||||
|     headings = ["To Invoice", "Payment ID", "Reference", "Amount"] |  | ||||||
|     data = [] |  | ||||||
| 
 |  | ||||||
|     payments = commerce.PaymentBase.objects.filter( |     payments = commerce.PaymentBase.objects.filter( | ||||||
|         invoice__user=attendee.user, |         invoice__user=attendee.user, | ||||||
|     ) |     ) | ||||||
|     for payment in payments: |     reports.append(QuerysetReport( | ||||||
|         data.append([ |         "Payments", | ||||||
|             payment.invoice.id, payment.id, payment.reference, payment.amount, |         ["invoice__id", "id", "reference", "amount"], | ||||||
|         ]) |         payments, | ||||||
| 
 |         link_view=views.invoice, | ||||||
|     reports.append( |     )) | ||||||
|         Report("Payments", headings, data, link_view="invoice") |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|     return reports |     return reports | ||||||
| 
 | 
 | ||||||
|  | @ -353,15 +329,20 @@ def attendee_list(request): | ||||||
| 
 | 
 | ||||||
|     data = [] |     data = [] | ||||||
| 
 | 
 | ||||||
|     for attendee in attendees: |     for a in attendees: | ||||||
|         data.append([ |         data.append([ | ||||||
|             attendee.user.id, |             a.user.id, | ||||||
|             attendee.attendeeprofilebase.attendee_name(), |             a.attendeeprofilebase.attendee_name(), | ||||||
|             attendee.user.email, |             a.user.email, | ||||||
|             attendee.has_registered > 0, |             a.has_registered > 0, | ||||||
|         ]) |         ]) | ||||||
| 
 | 
 | ||||||
|     # Sort by whether they've registered, then ID. |     # Sort by whether they've registered, then ID. | ||||||
|     data.sort(key=lambda attendee: (-attendee[3], attendee[0])) |     data.sort(key=lambda a: (-a[3], a[0])) | ||||||
| 
 | 
 | ||||||
|     return Report("Attendees", headings, data, link_view="attendee") |     class Report(ListReport): | ||||||
|  | 
 | ||||||
|  |         def get_link(self, argument): | ||||||
|  |             return reverse(self._link_view) + "?user=%d" % int(argument) | ||||||
|  | 
 | ||||||
|  |     return Report("Attendees", headings, data, link_view=attendee) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer