from __future__ import annotations
from typing import Any
from django.conf import settings
from django.contrib import messages
from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import URLPattern, URLResolver, path, reverse
from django.utils.formats import date_format
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from salesman.conf import app_settings
from salesman.core.utils import get_salesman_model
from salesman.orders.models import BaseOrder, BaseOrderItem
from .utils import format_price
[docs]class BaseAdminMixin:
"""
Mixin that adds formatters and display functions to the model admin.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
def get_queryset(self, request: HttpRequest) -> QuerySet[Any]:
self.request = request
return super().get_queryset(request) # type: ignore
[docs]class OrderItemAdminMixin(BaseAdminMixin):
"""
Admin mixin for Order Item model admin.
"""
def product_data_display(self, obj: BaseOrderItem) -> str:
return app_settings.SALESMAN_ADMIN_JSON_FORMATTER(
obj.product_data, context={"order_item": True}
)
product_data_display.short_description = _("Product data") # type: ignore
def unit_price_display(self, obj: BaseOrderItem) -> str:
return format_price(obj.unit_price, order=obj.order, request=self.request)
unit_price_display.short_description = _("Unit price") # type: ignore
def subtotal_display(self, obj: BaseOrderItem) -> str:
return format_price(obj.subtotal, order=obj.order, request=self.request)
subtotal_display.short_description = _("Subtotal") # type: ignore
def total_display(self, obj: BaseOrderItem) -> str:
return format_price(obj.total, order=obj.order, request=self.request)
total_display.short_description = _("Total") # type: ignore
def extra_display(self, obj: BaseOrderItem) -> str:
return app_settings.SALESMAN_ADMIN_JSON_FORMATTER(
obj.extra, context={"order_item": True}
)
extra_display.short_description = _("Extra") # type: ignore
def extra_rows_display(self, obj: BaseOrderItem) -> str:
return app_settings.SALESMAN_ADMIN_JSON_FORMATTER(
obj.extra_rows, context={"order_item": True}
)
extra_rows_display.short_description = _("Extra rows") # type: ignore
[docs]class OrderAdminMixin(BaseAdminMixin):
"""
Admin mixin for Order model admin.
"""
def extra_display(self, obj: BaseOrder) -> str:
return app_settings.SALESMAN_ADMIN_JSON_FORMATTER(
obj.extra, context={"order": True}
)
extra_display.short_description = _("Extra") # type: ignore
def extra_rows_display(self, obj: BaseOrder) -> str:
return app_settings.SALESMAN_ADMIN_JSON_FORMATTER(
obj.extra_rows, context={"order": True}
)
extra_rows_display.short_description = _("Extra rows") # type: ignore
def date_created_display(self, obj: BaseOrder) -> str:
return date_format(obj.date_created, format="DATETIME_FORMAT")
date_created_display.short_description = _("Date created") # type: ignore
def date_updated_display(self, obj: BaseOrder) -> str:
return date_format(obj.date_updated, format="DATETIME_FORMAT")
date_updated_display.short_description = _("Date updated") # type: ignore
def is_paid_display(self, obj: BaseOrder) -> bool:
return obj.is_paid
is_paid_display.boolean = True # type: ignore
is_paid_display.short_description = _("Is paid") # type: ignore
def customer_display(self, obj: BaseOrder) -> str:
if not obj.user:
return "-"
app_label, model_name = settings.AUTH_USER_MODEL.lower().split(".")
url = reverse(f"admin:{app_label}_{model_name}_change", args=[obj.user.id])
return mark_safe(f'<a href="{url}">{obj.user}</a>')
customer_display.short_description = _("Customer") # type: ignore
def shipping_address_display(self, obj: BaseOrder) -> str:
return mark_safe(obj.shipping_address.replace("\n", "<br>")) or "-"
shipping_address_display.short_description = _("Shipping address") # type: ignore
def billing_address_display(self, obj: BaseOrder) -> str:
return mark_safe(obj.billing_address.replace("\n", "<br>")) or "-"
billing_address_display.short_description = _("Billing address") # type: ignore
def subtotal_display(self, obj: BaseOrder) -> str:
return format_price(obj.subtotal, order=obj, request=self.request)
subtotal_display.short_description = _("Subtotal") # type: ignore
def total_display(self, obj: BaseOrder) -> str:
return format_price(obj.total, order=obj, request=self.request)
total_display.short_description = _("Total") # type: ignore
def amount_paid_display(self, obj: BaseOrder) -> str:
return format_price(obj.amount_paid, order=obj, request=self.request)
amount_paid_display.short_description = _("Amount paid") # type: ignore
def amount_outstanding_display(self, obj: BaseOrder) -> str:
return format_price(obj.amount_outstanding, order=obj, request=self.request)
amount_outstanding_display.short_description = _("Amount outstanding") # type: ignore # noqa
[docs]class OrderAdminRefundMixin:
"""
Mixin to add refund functionality to Order admin.
"""
def get_urls(self) -> list[URLPattern | URLResolver]:
urls: list[URLPattern | URLResolver] = super().get_urls() # type: ignore
urls += [
path(
"<path:object_id>/refund/",
self.admin_site.admin_view(self.refund_view), # type: ignore
name="salesman_order_refund",
),
]
return urls
def refund_view(self, request: HttpRequest, object_id: int) -> HttpResponse:
Order = get_salesman_model("Order")
order = get_object_or_404(Order, id=object_id)
app_label, model_name = app_settings.SALESMAN_ORDER_MODEL.lower().split(".")
object_url = reverse(f"admin:{app_label}_{model_name}_change", args=[object_id])
if "_refund-error" in request.POST:
# Refund error, add error message and redirect to change view.
msg = _("There was an error while trying to refund order.")
self.message_user( # type: ignore
request, msg, messages.ERROR, fail_silently=True
)
return redirect(object_url)
if "_refund-success" in request.POST:
# Refund success, add success message and redirect to change view.
failed = int(request.POST["_refund-success"])
if failed:
msg = _("The Order “{}” was only partially refunded.")
status = messages.WARNING
else:
msg = _("The Order “{}” was successfully refunded.")
status = messages.SUCCESS
self.message_user( # type: ignore
request, msg.format(order), status, fail_silently=True
)
return redirect(object_url)
context = {
"title": _("Refund Order"),
"object": order,
"media": self.media, # type: ignore
"opts": self.model._meta, # type: ignore
"object_url": object_url,
}
return render(request, "salesman/admin/refund.html", context)