Changeset - ea07469634ee
[Not reviewed]
0 2 0
Christopher Neugebauer - 8 years ago 2016-12-06 23:18:48
chrisjrn@gmail.com
Fixes individual attendee view, which had disappeared.
2 files changed with 7 insertions and 2 deletions:
0 comments (0 inline, 0 general)
registrasion/reporting/reports.py
Show inline comments
...
 
@@ -40,273 +40,276 @@ class Report(object):
 
            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._headings = headings
 
        self._link_view = link_view
 

	
 
    def title(self):
 
        ''' Returns the title for this report. '''
 
        return self._title
 

	
 
    def headings(self):
 
        ''' Returns the headings for the table. '''
 
        return self._headings
 

	
 
    def cell_text(self, content_type, index, text):
 
        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. '''
 

	
 
        for row in self._data:
 
            yield [
 
                self.cell_text(content_type, i, cell)
 
                for i, cell in enumerate(row)
 
            ]
 

	
 

	
 
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):
 
    ''' Decorator that converts a report view function into something that
 
    displays a Report.
 

	
 
    Arguments:
 
        title (str):
 
            The title of the report.
 
        form_type (Optional[forms.Form]):
 
            A form class that can make this report display things. If not
 
            supplied, no form will be displayed.
 

	
 
    '''
 

	
 
    # Create & return view
 
    def _report(view):
 
        report_view = ReportView(view, title, form_type)
 
        report_view = user_passes_test(views._staff_only)(report_view)
 
        report_view = wraps(view)(report_view)
 

	
 
        # Add this report to the list of reports.
 
        _all_report_views.append(report_view)
 

	
 
        return report_view
 

	
 
    return _report
 

	
 

	
 
class ReportView(object):
 

	
 
    def __init__(self, inner_view, title, form_type):
 
        # Consolidate form_type so it has content type and section
 
        self.inner_view = inner_view
 
        self.title = title
 
        self.form_type = form_type
 

	
 
    def __call__(self, request, *a, **k):
 
        data = ReportViewRequestData(self, request, *a, **k)
 
        return self.render(data)
 

	
 
    def get_form(self, request):
 

	
 
        # Create a form instance
 
        if self.form_type is not None:
 
            form = self.form_type(request.GET)
 

	
 
            # Pre-validate it
 
            form.is_valid()
 
        else:
 
            form = None
 

	
 
        return form
 

	
 
    @classmethod
 
    def wrap_reports(cls, reports, content_type):
 
        reports = [
 
            _ReportTemplateWrapper(content_type, report)
 
            for report in reports
 
        ]
 

	
 
        return reports
 

	
 
    def render(self, data):
 
        renderers = {
 
            "text/csv": self._render_as_csv,
 
            "text/html": self._render_as_html,
 
            None: self._render_as_html,
 
        }
 
        render = renderers[data.content_type]
 
        return render(data)
 

	
 
    def _render_as_html(self, data):
 
        ctx = {
 
            "title": self.title,
 
            "form": data.form,
 
            "reports": data.reports,
 
        }
 

	
 
        return render(data.request, "registrasion/report.html", ctx)
 

	
 
    def _render_as_csv(self, data):
 
        report = data.reports[data.section]
 

	
 
        # Create the HttpResponse object with the appropriate CSV header.
 
        response = HttpResponse(content_type='text/csv')
 

	
 
        writer = csv.writer(response)
 
        encode = lambda i: i.encode("utf8") if isinstance(i, unicode) else i
 
        writer.writerow(list(encode(i) for i in report.headings()))
 
        for row in report.rows():
 
            writer.writerow(list(encode(i) for i in row))
 

	
 
        return response
 

	
 

	
 
class ReportViewRequestData(object):
 

	
 
    def __init__(self, report_view, request, *a, **k):
 
        self.report_view = report_view
 
        self.request = request
 

	
 
        # Calculate other data
 
        self.form = report_view.get_form(request)
 

	
 
        # Content type and section come from request.GET
 
        self.content_type = request.GET.get("content_type")
 
        self.section = request.GET.get("section")
 
        self.section = int(self.section) if self.section else None
 

	
 
        if self.content_type is None:
 
            self.content_type = "text/html"
 

	
 
        # Reports come from calling the inner view
 
        reports = report_view.inner_view(request, self.form, *a, **k)
 

	
 
        # Normalise to a list
 
        if isinstance(reports, Report):
 
            reports = [reports]
 

	
 
        # Wrap them in appropriate format
 
        reports = ReportView.wrap_reports(reports, self.content_type)
 

	
 
        self.reports = reports
 

	
 

	
 
def get_all_reports():
 
    ''' Returns all the views that have been registered with @report '''
 

	
 
    return list(_all_report_views)
registrasion/reporting/views.py
Show inline comments
...
 
@@ -155,517 +155,519 @@ def sales_payment_summary():
 

	
 
    data.append(["Items on paid invoices", sales])
 
    data.append(["All payments", all_payments])
 
    data.append(["Sales - Payments ", sales - all_payments])
 
    data.append(["All credit notes", all_credit_notes])
 
    data.append(["Credit notes paid on invoices", claimed_credit_notes])
 
    data.append(["Credit notes refunded", refunded_credit_notes])
 
    data.append(["Unclaimed credit notes", unclaimed_credit_notes])
 
    data.append([
 
        "Credit notes - (claimed credit notes + unclaimed credit notes)",
 
        all_credit_notes - claimed_credit_notes -
 
            refunded_credit_notes - unclaimed_credit_notes,
 
    ])
 

	
 
    return ListReport("Sales and Payments Summary", headings, data)
 

	
 

	
 
def payments():
 
    ''' Shows the history of payments into the system '''
 

	
 
    payments = commerce.PaymentBase.objects.all()
 
    return QuerysetReport(
 
        "Payments",
 
        ["invoice__id", "id", "reference", "amount"],
 
        payments,
 
        link_view=views.invoice,
 
    )
 

	
 

	
 
def credit_note_refunds():
 
    ''' Shows all of the credit notes that have been generated. '''
 
    notes_refunded = commerce.CreditNote.refunded()
 
    return QuerysetReport(
 
        "Credit note refunds",
 
        ["id", "creditnoterefund__reference", "amount"],
 
        notes_refunded,
 
        link_view=views.credit_note,
 
    )
 

	
 

	
 
def group_by_cart_status(queryset, order, values):
 
    queryset = queryset.annotate(
 
        is_reserved=Case(
 
            When(cart__in=commerce.Cart.reserved_carts(), then=Value(True)),
 
            default=Value(False),
 
            output_field=models.BooleanField(),
 
        ),
 
    )
 

	
 
    values = queryset.order_by(*order).values(*values)
 
    values = values.annotate(
 
        total_paid=Sum(Case(
 
            When(
 
                cart__status=commerce.Cart.STATUS_PAID,
 
                then=F("quantity"),
 
            ),
 
            default=Value(0),
 
        )),
 
        total_refunded=Sum(Case(
 
            When(
 
                cart__status=commerce.Cart.STATUS_RELEASED,
 
                then=F("quantity"),
 
            ),
 
            default=Value(0),
 
        )),
 
        total_unreserved=Sum(Case(
 
            When(
 
                (
 
                    Q(cart__status=commerce.Cart.STATUS_ACTIVE) &
 
                    Q(is_reserved=False)
 
                ),
 
                then=F("quantity"),
 
            ),
 
            default=Value(0),
 
        )),
 
        total_reserved=Sum(Case(
 
            When(
 
                (
 
                    Q(cart__status=commerce.Cart.STATUS_ACTIVE) &
 
                    Q(is_reserved=True)
 
                ),
 
                then=F("quantity"),
 
            ),
 
            default=Value(0),
 
        )),
 
    )
 

	
 
    return values
 

	
 

	
 
@report_view("Product status", form_type=forms.ProductAndCategoryForm)
 
def product_status(request, form):
 
    ''' Summarises the inventory status of the given items, grouping by
 
    invoice status. '''
 

	
 
    products = form.cleaned_data["product"]
 
    categories = form.cleaned_data["category"]
 

	
 
    items = commerce.ProductItem.objects.filter(
 
        Q(product__in=products) | Q(product__category__in=categories),
 
    ).select_related("cart", "product")
 

	
 
    items = group_by_cart_status(
 
        items,
 
        ["product__category__order", "product__order"],
 
        ["product", "product__category__name", "product__name"],
 
    )
 

	
 
    headings = [
 
        "Product", "Paid", "Reserved", "Unreserved", "Refunded",
 
    ]
 
    data = []
 

	
 
    for item in items:
 
        data.append([
 
            "%s - %s" % (
 
                item["product__category__name"], item["product__name"]
 
            ),
 
            item["total_paid"],
 
            item["total_reserved"],
 
            item["total_unreserved"],
 
            item["total_refunded"],
 
        ])
 

	
 
    return ListReport("Inventory", headings, data)
 

	
 

	
 
@report_view("Product status", form_type=forms.DiscountForm)
 
def discount_status(request, form):
 
    ''' Summarises the usage of a given discount. '''
 

	
 
    discounts = form.cleaned_data["discount"]
 

	
 
    items = commerce.DiscountItem.objects.filter(
 
        Q(discount__in=discounts),
 
    ).select_related("cart", "product", "product__category")
 

	
 
    items = group_by_cart_status(
 
        items,
 
        ["discount",],
 
        ["discount", "discount__description",],
 
    )
 

	
 
    headings = [
 
        "Discount", "Paid", "Reserved", "Unreserved", "Refunded",
 
    ]
 
    data = []
 

	
 
    for item in items:
 
        data.append([
 
            item["discount__description"],
 
            item["total_paid"],
 
            item["total_reserved"],
 
            item["total_unreserved"],
 
            item["total_refunded"],
 
        ])
 

	
 
    return ListReport("Usage by item", headings, data)
 

	
 

	
 
@report_view("Paid invoices by date", form_type=forms.ProductAndCategoryForm)
 
def paid_invoices_by_date(request, form):
 
    ''' Shows the number of paid invoices containing given products or
 
    categories per day. '''
 

	
 
    products = form.cleaned_data["product"]
 
    categories = form.cleaned_data["category"]
 

	
 
    invoices = commerce.Invoice.objects.filter(
 
        (
 
            Q(lineitem__product__in=products) |
 
            Q(lineitem__product__category__in=categories)
 
        ),
 
        status=commerce.Invoice.STATUS_PAID,
 
    )
 

	
 
    # Invoices with payments will be paid at the time of their latest payment
 
    payments = commerce.PaymentBase.objects.all()
 
    payments = payments.filter(
 
        invoice__in=invoices,
 
    )
 
    payments = payments.order_by("invoice")
 
    invoice_max_time = payments.values("invoice").annotate(
 
        max_time=Max("time")
 
    )
 

	
 
    # Zero-value invoices will have no payments, so they're paid at issue time
 
    zero_value_invoices = invoices.filter(value=0)
 

	
 
    times = itertools.chain(
 
        (line["max_time"] for line in invoice_max_time),
 
        (invoice.issue_time for invoice in zero_value_invoices),
 
    )
 

	
 
    by_date = collections.defaultdict(int)
 
    for time in times:
 
        date = datetime.datetime(
 
            year=time.year, month=time.month, day=time.day
 
        )
 
        by_date[date] += 1
 

	
 
    data = [(date, count) for date, count in sorted(by_date.items())]
 
    data = [(date.strftime("%Y-%m-%d"), count) for date, count in data]
 

	
 
    return ListReport(
 
        "Paid Invoices By Date",
 
        ["date", "count"],
 
        data,
 
    )
 

	
 
@report_view("Credit notes")
 
def credit_notes(request, form):
 
    ''' Shows all of the credit notes in the system. '''
 

	
 
    notes = commerce.CreditNote.objects.all().select_related(
 
        "creditnoterefund",
 
        "creditnoteapplication",
 
        "invoice",
 
        "invoice__user__attendee__attendeeprofilebase",
 
    )
 

	
 
    return QuerysetReport(
 
        "Credit Notes",
 
        ["id", "invoice__user__attendee__attendeeprofilebase__invoice_recipient", "status", "value"],  # NOQA
 
        notes,
 
        headings=["id", "Owner", "Status", "Value"],
 
        link_view=views.credit_note,
 
    )
 

	
 

	
 
@report_view("Invoices")
 
def invoices(request,form):
 
    ''' Shows all of the invoices in the system. '''
 

	
 
    invoices = commerce.Invoice.objects.all().order_by("status")
 

	
 
    return QuerysetReport(
 
        "Invoices",
 
        ["id", "recipient", "value", "get_status_display"],
 
        invoices,
 
        headings=["id", "Recipient", "Value", "Status"],
 
        link_view=views.invoice,
 
    )
 

	
 

	
 
class AttendeeListReport(ListReport):
 

	
 
    def get_link(self, argument):
 
        return reverse(self._link_view) + "?user=%d" % int(argument)
 

	
 

	
 
@report_view("Attendee", form_type=forms.UserIdForm)
 
def attendee(request, form, user_id=None):
 
    ''' Returns a list of all manifested attendees if no attendee is specified,
 
    else displays the attendee manifest. '''
 

	
 
    if user_id is None and form.cleaned_data["user"] is not None:
 
        user_id = form.cleaned_data["user"]
 

	
 
    if user_id is None:
 
        return attendee_list(request)
 

	
 
    if form.cleaned_data["user"] is not None:
 
        user_id = form.cleaned_data["user"]
 
    print user_id
 

	
 
    attendee = people.Attendee.objects.get(user__id=user_id)
 
    name = attendee.attendeeprofilebase.attendee_name()
 

	
 
    reports = []
 

	
 
    profile_data = []
 
    try:
 
        profile = people.AttendeeProfileBase.objects.get_subclass(
 
            attendee=attendee
 
        )
 
        fields = profile._meta.get_fields()
 
    except people.AttendeeProfileBase.DoesNotExist:
 
        fields = []
 

	
 
    exclude = set(["attendeeprofilebase_ptr", "id"])
 
    for field in fields:
 
        if field.name in exclude:
 
            # Not actually important
 
            continue
 
        if not hasattr(field, "verbose_name"):
 
            continue  # Not a publicly visible field
 
        value = getattr(profile, field.name)
 

	
 
        if isinstance(field, models.ManyToManyField):
 
            value = ", ".join(str(i) for i in value.all())
 

	
 
        profile_data.append((field.verbose_name, value))
 

	
 
    cart = CartController.for_user(attendee.user)
 
    reservation = cart.cart.reservation_duration + cart.cart.time_last_updated
 
    profile_data.append(("Current cart reserved until", reservation))
 

	
 
    reports.append(ListReport("Profile", ["", ""], profile_data))
 

	
 
    links = []
 
    links.append((
 
        reverse(views.amend_registration, args=[user_id]),
 
        "Amend current cart",
 
    ))
 
    links.append((
 
        reverse(views.extend_reservation, args=[user_id]),
 
        "Extend reservation",
 
    ))
 

	
 
    reports.append(Links("Actions for " + name, links))
 

	
 
    # Paid and pending  products
 
    ic = ItemController(attendee.user)
 
    reports.append(ListReport(
 
        "Paid Products",
 
        ["Product", "Quantity"],
 
        [(pq.product, pq.quantity) for pq in ic.items_purchased()],
 
    ))
 
    reports.append(ListReport(
 
        "Unpaid Products",
 
        ["Product", "Quantity"],
 
        [(pq.product, pq.quantity) for pq in ic.items_pending()],
 
    ))
 

	
 
    # Invoices
 
    invoices = commerce.Invoice.objects.filter(
 
        user=attendee.user,
 
    )
 
    reports.append(QuerysetReport(
 
        "Invoices",
 
        ["id", "get_status_display", "value"],
 
        invoices,
 
        headings=["Invoice ID", "Status", "Value"],
 
        link_view=views.invoice,
 
    ))
 

	
 
    # Credit Notes
 
    credit_notes = commerce.CreditNote.objects.filter(
 
        invoice__user=attendee.user,
 
    ).select_related("invoice", "creditnoteapplication", "creditnoterefund")
 

	
 
    reports.append(QuerysetReport(
 
        "Credit Notes",
 
        ["id", "status", "value"],
 
        credit_notes,
 
        link_view=views.credit_note,
 
    ))
 

	
 
    # All payments
 
    payments = commerce.PaymentBase.objects.filter(
 
        invoice__user=attendee.user,
 
    ).select_related("invoice")
 

	
 
    reports.append(QuerysetReport(
 
        "Payments",
 
        ["invoice__id", "id", "reference", "amount"],
 
        payments,
 
        link_view=views.invoice,
 
    ))
 

	
 
    return reports
 

	
 

	
 
def attendee_list(request):
 
    ''' Returns a list of all attendees. '''
 

	
 
    attendees = people.Attendee.objects.select_related(
 
        "attendeeprofilebase",
 
        "user",
 
    )
 

	
 
    profiles = AttendeeProfile.objects.filter(
 
        attendee__in=attendees
 
    ).select_related(
 
        "attendee", "attendee__user",
 
    )
 
    profiles_by_attendee = dict((i.attendee, i) for i in profiles)
 

	
 
    attendees = attendees.annotate(
 
        has_registered=Count(
 
            Q(user__invoice__status=commerce.Invoice.STATUS_PAID)
 
        ),
 
    )
 

	
 
    headings = [
 
        "User ID", "Name", "Email", "Has registered",
 
    ]
 

	
 
    data = []
 

	
 
    for a in attendees:
 
        data.append([
 
            a.user.id,
 
            (profiles_by_attendee[a].attendee_name()
 
                if a in profiles_by_attendee else ""),
 
            a.user.email,
 
            a.has_registered > 0,
 
        ])
 

	
 
    # Sort by whether they've registered, then ID.
 
    data.sort(key=lambda a: (-a[3], a[0]))
 

	
 
    return AttendeeListReport("Attendees", headings, data, link_view=attendee)
 

	
 

	
 
ProfileForm = forms.model_fields_form_factory(AttendeeProfile)
 

	
 
@report_view(
 
    "Attendees By Product/Category",
 
    form_type=forms.mix_form(
 
        forms.ProductAndCategoryForm, ProfileForm, forms.GroupByForm
 
    ),
 
)
 
def attendee_data(request, form, user_id=None):
 
    ''' Lists attendees for a given product/category selection along with
 
    profile data.'''
 

	
 
    status_display = {
 
        commerce.Cart.STATUS_ACTIVE: "Unpaid",
 
        commerce.Cart.STATUS_PAID: "Paid",
 
        commerce.Cart.STATUS_RELEASED: "Refunded",
 
    }
 

	
 
    output = []
 

	
 
    by_category = form.cleaned_data["group_by"] == forms.GroupByForm.GROUP_BY_CATEGORY
 

	
 
    products = form.cleaned_data["product"]
 
    categories = form.cleaned_data["category"]
 
    fields = form.cleaned_data["fields"]
 
    name_field = AttendeeProfile.name_field()
 

	
 
    items = commerce.ProductItem.objects.filter(
 
        Q(product__in=products) | Q(product__category__in=categories),
 
    ).exclude(
 
        cart__status=commerce.Cart.STATUS_RELEASED
 
    ).select_related(
 
        "cart", "cart__user", "product", "product__category",
 
    ).order_by("cart__status")
 

	
 
    # Make sure we select all of the related fields
 
    related_fields = set(
 
        field for field in fields
 
        if isinstance(AttendeeProfile._meta.get_field(field), RelatedField)
 
    )
 

	
 
    # Get all of the relevant attendee profiles in one hit.
 
    profiles = AttendeeProfile.objects.filter(
 
        attendee__user__cart__productitem__in=items
 
    ).select_related("attendee__user").prefetch_related(*related_fields)
 
    by_user = {}
 
    for profile in profiles:
 
        by_user[profile.attendee.user] = profile
 

	
 
    cart = "attendee__user__cart"
 
    cart_status = cart + "__status"
 
    product = cart + "__productitem__product"
 
    product_name = product + "__name"
 
    category = product + "__category"
 
    category_name = category + "__name"
 

	
 
    if by_category:
 
        grouping_fields = (category, category_name)
 
        order_by = (category, )
 
        first_column = "Category"
 
        group_name = lambda i: "%s" % (i[category_name], )
 
    else:
 
        grouping_fields = (product, product_name, category_name)
 
        order_by = (category, )
 
        first_column = "Product"
 
        group_name = lambda i: "%s - %s" % (i[category_name], i[product_name])
 

	
 
    # Group the responses per-field.
 
    for field in fields:
 
        concrete_field = AttendeeProfile._meta.get_field(field)
 
        field_verbose = concrete_field.verbose_name
 

	
 
        # Render the correct values for related fields
 
        if field in related_fields:
 
            # Get all of the IDs that will appear
 
            all_ids = profiles.order_by(field).values(field)
 
            all_ids = [i[field] for i in all_ids if i[field] is not None]
 
            # Get all of the concrete objects for those IDs
 
            model = concrete_field.related_model
 
            all_objects = model.objects.filter(id__in=all_ids)
 
            all_objects_by_id = dict((i.id, i) for i in all_objects)
 

	
 
            # Define a function to render those IDs.
 
            def display_field(value):
 
                if value in all_objects_by_id:
 
                    return all_objects_by_id[value]
 
                else:
 
                    return None
 
        else:
 
            def display_field(value):
 
                return value
 

	
 
        status_count = lambda status: Case(When(
 
                attendee__user__cart__status=status,
 
                then=Value(1),
 
            ),
 
            default=Value(0),
 
            output_field=models.fields.IntegerField(),
 
        )
 
        paid_count = status_count(commerce.Cart.STATUS_PAID)
 
        unpaid_count = status_count(commerce.Cart.STATUS_ACTIVE)
 

	
 
        groups = profiles.order_by(
 
            *(order_by + (field, ))
 
        ).values(
 
            *(grouping_fields + (field, ))
 
        ).annotate(
 
            paid_count=Sum(paid_count),
 
            unpaid_count=Sum(unpaid_count),
 
        )
 
        output.append(ListReport(
 
            "Grouped by %s" % field_verbose,
 
            [first_column, field_verbose, "paid", "unpaid"],
 
            [
 
                (
0 comments (0 inline, 0 general)