10

I have a three-levels Invoice model which I'd like to display on Django's admin area... in a "kind of special" way.

Allow me to provide a bit of background:

Each Invoice is conformed by several SubInvoice(s), and each SubInvoice is conformed by several InvoiceItem(s), which contain a break down of the Products purchased by a customer.

Logically speaking, it'd be something like this (hopefully the ascii art works)

+---------- Invoice id=3 -----------+
|       Full total: $100.00         |
|                                   |
|  +----- Sub Invoice id=1 -----+   |
|  |      Subtotal $70          |   |
|  |                            |   |
|  |    Item 1 in SubInv.1      |   |
|  |    Item 2 in SubInv.1      |   |
|  |    Item 3 in SubInv.1      |   |
|  |____________________________|   |
|                                   |
|  +----- Sub Invoice id=2 -----+   |
|  |      Subtotal $30          |   |
|  |                            |   |
|  |    Item 1 in SubInv.2      |   |
|  |    Item 2 in SubInv.2      |   |
|  |____________________________|   |
|                                   |
|___________________________________|

The models look more or less (they've been simplified for this question) like:

class Invoice(models.Model):
    full_total = DecimalField(...)
    # has a .sub_invoices RelatedManager through a backref from SubInvoice

class SubInvoice(models.Model):
    sub_total = DecimalField(...)
    invoice = ForeignKey('server.Invoice', related_name='sub_invoices')
    # has an .items RelatedManager through a backref from InvoiceItem

class InvoiceItem(models.Model):
    sub_invoice = ForeignKey('server.SubInvoice', related_name='items')
    product = ForeignKey('server.Product', related_name='+')
    quantity = PositiveIntegerField(...)
    price = DecimalField(...)

Now, I am aware that nesting two levels of relationships in Django Admin is very complex, and I'm not trying to nest the InvoiceItem into the SubInvoice and nest that one into the Invoice. That'd be great, but I'm ready to give that up due to the difficulties of nested inlines. No: what I'd like to to do is showing the Invoice and, as an inline, its Items, "jumping" through Invoice.sub_invoices__items. I don't really care that much about the information shown in the SubInvoice(s), but I do care about the information in the Invoice and in the InvoiceItems.

What I mean is that, basically, I would like (or "I could live with", rather) if the Invoice admin view looked like the following:

+---------- Invoice id=3 -----------+
|       Full total: $100.00         |
|                                   |
|  +----------------------------+   |
|  |                            |   |
|  |    Item 1 in SubInv.1      |   |
|  |    Item 2 in SubInv.1      |   |
|  |    Item 3 in SubInv.1      |   |
|  |    Item 1 in SubInv.2      |   |
|  |    Item 2 in SubInv.2      |   |
|  |____________________________|   |
|                                   |
|___________________________________|

(InvoiceItems as an inline of the Invoice(s) without showing any information about the SubInvoices in it)

I've tried the following in the admin.py:

class InvoiceItemInline(admin.StackedInline):
    fk_name = 'sub_invoice__invoice'
    model = InvoiceItem

class InvoiceAdmin(admin.ModelAdmin):
    inlines = (InvoiceItemInline,)

But that gives me an error:

<class 'server.admin.invoices.InvoiceItemInline'>: (admin.E202) 'server.InvoiceItem' has no field named 'sub_invoice__invoice'.

I've also tried directly this:

class InvoiceItemInline(admin.StackedInline):
    model = InvoiceItem

class InvoiceAdmin(admin.ModelAdmin):
    inlines = (InvoiceItemInline,)

But then (this one I was expecting) produces this error:

<class 'server.admin.invoices.InvoiceItemInline'>: (admin.E202) 'server.InvoiceItem' has no ForeignKey to 'server.Invoice'.

Is there any way of achieving this? Thank you in advance.

PS:

As of now, I have a "patched" solution which seems to be the canonical way:

  • Register the Invoice model.
  • Register an admin.ModelAdmin inline for the SubInvoice (this inline will be "inlined" into the Invoice's ModelAdmin).
  • Also register the SubInvoice in the admin, so we can calculate a link to its admin view.
  • Add an inline view of the InvoiceItems to the aforementioned SubInvoice's view.
  • Add a link to the admin view of the SubInvoice(s) in the Invoice

Pretty much what is described in this other S.O. answer.

But the problem with this approach is that it won't let me see the Invoice and its InvoiceItemsat a glance (I see the invoice, with sub_invoices in it, and then within the sub_invoices inlines, there's a link to the InvoiceItems which I have to click on in order to see the items). It'd be great if I could get rid of the need for that link.

This is what I have now, basically:

+---------- Invoice id=3 -----------+
|       Full total: $100.00         |
|                                   |
|  +----- Sub Invoice id=1 -----+   |       +--- Sub Invoice id=1 ---+
|  |      Subtotal $70          |   |       |   Item 1 in SubInv.1   |
|  |                            |   |       |   Item 2 in SubInv.1   |
|  |    <a>Click for items ==============>  |   Item 3 in SubInv.1   |
|  |____________________________|   |       |________________________|
|                                   |
|  +----- Sub Invoice id=2 -----+   |
|  |      Subtotal $30          |   |       +--- Sub Invoice id=2 ---+
|  |                            |   |       |   Item 1 in SubInv.2   |
|  |    <a>Click for items ==============>  |   Item 2 in SubInv.2   |
|  |____________________________|   |       |________________________|
|                                   |
|___________________________________|
Savir
  • 17,568
  • 15
  • 82
  • 136
  • 4
    You can try django-nested-admin (https://github.com/theatlantic/django-nested-admin), which provides support for two-level inlines – Don Nov 14 '17 at 16:44

1 Answers1

2

I think your problem could be solved using the ManyToManyField + through. (This is an example)

#models.py
class Invoice(models.Model):
    full_total = DecimalField(...)
    # has a .sub_invoices RelatedManager through a backref from SubInvoice

class SubInvoice(models.Model):
    sub_total = DecimalField(...)
    invoice = ManyToManyField(
        'server.Invoice',
        through='server.InvoiceItem',
        through_fields=('sub_invoice', 'invoice'))
    # has an .items RelatedManager through a backref from InvoiceItem

class InvoiceItem(models.Model):
    sub_invoice = ForeignKey('server.SubInvoice')
    invoice = ForeignKey('server.Invoice')
    product = ForeignKey('server.Product', related_name='+')
    quantity = PositiveIntegerField(...)
    price = DecimalField(...)

#admin.py
from django.contrib import admin
from .models import InvoiceItem, Invoice, SubInvoice


class InvoiceItemInline(admin.TabularInline):
    model = InvoiceItem
    extra = 1


class InvoiceAdmin(admin.ModelAdmin):
    inlines = (InvoiceItemInline,)


admin.site.register(Invoice, InvoiceAdmin)
admin.site.register(SubInvoice, InvoiceAdmin)

I would recommend working with classes for this one in your views.py and use inlineformset_factory in your forms.py for this. Using jquery-formset library in your frontend as well, as this looks pretty neat together.

NOTE: You can also use on_delete=models.CASCADE if you want in the InvoiceItem on the foreignkeys, so if one of those items is deleted, then the InvoiceItem will be deleted too, or models.SET_NULL, whichever you prefer.

Hope this might help you out.

King Reload
  • 2,780
  • 1
  • 17
  • 42
  • Thanks for the reply. I was trying to keep the `InvoiceItem` normalized (just with the `sub_invoice` FK, and not having to add the `invoice` also) – Savir Apr 06 '18 at 00:08
  • The `Invoice` isn't visible when you click on `Invoice` in the admin view, rather you will see all the `SubInvoices` with the `InvoiceItems` linked to the `Invoice` and in the `SubInvoice` you will see which `Invoice` and `InvoiceItem` it's linked to. Maybe you could do a test with this to see the results of what I mean? Because now you don't have to register the `InvoiceItem` in your `admin.py` to be able to see it. I would love to figure your problem out as I'm very much so interested in this :) – King Reload Apr 06 '18 at 06:34
  • Sorry: I was actually talking about the models: In your code, the `InvoiceItem` must have two foreign keys: One to `SubInvoice` and another one to `Invoice`, which de-normalizes the tables since `InvoiceItem.invoice` is "redundant" (redundant in the sense that the `invoice` the `InvoiceItem` belongs to is **also** reachable through `InvoiceItem --> sub_invoice --> invoice` so in theory, I could force an InvoiceItem to belong to a `subinvoice` that doesn't actually belong to the same `invoice`, right? (maybe I'm looking at this wrong, though) – Savir Apr 06 '18 at 11:12
  • My concern (to further clarify the comment above) is: What happens if I stick the wrong value (let's say I put a... dunno... `invoice_id=42`) in `InvoiceItem.invoice` and then I have a situation in which, following the path `InvoiceItem --> sub_invoice --> invoice` I reach the `invoice` with ID 3, but following the path `InvoiceItem --> invoice`, I reach the `invoice` with ID 42 – Savir Apr 06 '18 at 11:16
  • In the example I gave you the `SubInvoice` has `full_total`, `product`, `quantity` and `price`. The `Invoice` has `sub_total`, `product`, `quantity` and `price`. In this case the SubInvoice, Invoice and InvoiceItem will all three still have use for each other. In your examples it seems like you mean to use this as well. Like this there also won't be any ID problems. I could post an image of how it looks in the database if you want to? – King Reload Apr 06 '18 at 11:23
  • Yeah, those fields are used, and your example is fine in that regard: My concern comes because of the presence of the two FK (`.invoice` and `.sub_invoice`) in the `InvoiceItem`. Without `InvoiceItem.invoice`, the question _What `Invoice` does this `InvoiceItem` belongs to?_ can uniquely be answered through `InvoiceItem.sub_invoice.invoice` But if I add the `InvoiceItem.invoice` FK, now I can (potentially) have 2 paths to reach the item's `invoice`: One, through `InvoiceItem.sub_invoice.invoice` and another one through `InvoiceItem.invoice`, no? If so, it feels it lead to potential bugs? – Savir Apr 06 '18 at 11:32
  • You can call to `InvoiceItem.invoice` and `SubInvoice.through.invoice` in this case, as for *what* `Invoice` the `InvoiceItem` belongs to is up to you which `Invoice` you want it to belong to. – King Reload Apr 06 '18 at 11:40
  • You can however also have the `ManyToManyField` in the `InvoiceItem` refering to `SubInvoice` going through `Invoice` which will create a whole other scenario, like I said the code I wrote is just an example of how you can use a `ManyToManyField`, which I think can also do the thing that you want to establish. – King Reload Apr 06 '18 at 11:54