import logging from django.core.exceptions import ImproperlyConfigured,ValidationError from django.contrib.auth.mixins import LoginRequiredMixin,PermissionRequiredMixin from django_ledger.forms.bill import ( BillModelCreateForm, BaseBillModelUpdateForm, DraftBillModelUpdateForm, get_bill_itemtxs_formset_class, BillModelConfigureForm, InReviewBillModelUpdateForm, ApprovedBillModelUpdateForm, AccruedAndApprovedBillModelUpdateForm, PaidBillModelUpdateForm ) from django.http import HttpResponseForbidden from django.utils.html import format_html from django.contrib import messages from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse from django_ledger.models import ItemTransactionModel from django.views.generic.detail import DetailView from django_ledger.forms.purchase_order import (ApprovedPurchaseOrderModelUpdateForm, BasePurchaseOrderModelUpdateForm, DraftPurchaseOrderModelUpdateForm, ReviewPurchaseOrderModelUpdateForm, get_po_itemtxs_formset_class) from django_ledger.views.purchase_order import PurchaseOrderModelModelViewQuerySetMixIn from django_ledger.models import PurchaseOrderModel,EstimateModel,BillModel from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import UpdateView from django.views.generic.base import RedirectView from .models import Dealer logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) class PurchaseOrderModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): slug_url_kwarg = 'po_pk' slug_field = 'uuid' context_object_name = 'po_model' template_name = "purchase_orders/po_update.html" context_object_name = "po_model" permission_required = "django_ledger.change_purchaseordermodel" extra_context = { 'header_subtitle_icon': 'uil:bill' } action_update_items = False queryset = None def get_context_data(self, itemtxs_formset=None, **kwargs): dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) context = super().get_context_data(**kwargs) context["entity_slug"] = dealer.entity.slug po_model: PurchaseOrderModel = self.object if not itemtxs_formset: itemtxs_qs = self.get_po_itemtxs_qs(po_model) itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data(queryset=itemtxs_qs) po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model) itemtxs_formset = po_itemtxs_formset_class( entity_slug=dealer.entity.slug, user_model=dealer.entity.admin, po_model=po_model, queryset=itemtxs_qs, ) else: itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data() context["itemtxs_qs"] = itemtxs_qs context["itemtxs_formset"] = itemtxs_formset return context def get_queryset(self): dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) if self.queryset is None: self.queryset = PurchaseOrderModel.objects.for_entity( entity_slug=self.kwargs['entity_slug'], user_model=dealer.entity.admin ).select_related('entity', 'ce_model') return super().get_queryset() def get_success_url(self): return reverse( "purchase_order_update", kwargs={ "dealer_slug": self.kwargs["dealer_slug"], "entity_slug": self.kwargs["entity_slug"], "po_pk": self.kwargs["po_pk"], }, ) def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs): if self.action_update_items: return HttpResponseRedirect( redirect_to=reverse( "purchase_order_update", kwargs={ "dealer_slug": dealer_slug, "entity_slug": entity_slug, "po_pk": po_pk, }, ) ) return super(PurchaseOrderModelUpdateView, self).get( request, dealer_slug, entity_slug, po_pk, *args, **kwargs ) def post(self, request, dealer_slug, entity_slug, *args, **kwargs): if self.action_update_items: if not request.user.is_authenticated: return HttpResponseForbidden() queryset = self.get_queryset() po_model: PurchaseOrderModel = self.get_object(queryset=queryset) self.object = po_model po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model) itemtxs_formset = po_itemtxs_formset_class( request.POST, user_model=request.dealer.entity.admin, po_model=po_model, entity_slug=entity_slug, ) if itemtxs_formset.has_changed(): if itemtxs_formset.is_valid(): itemtxs_list = itemtxs_formset.save(commit=False) create_bill_uuids = [ str(i["uuid"].uuid) for i in itemtxs_formset.cleaned_data if i and i["create_bill"] is True ] if create_bill_uuids: item_uuids = ",".join(create_bill_uuids) redirect_url = reverse( "bill-create-po", kwargs={ "dealer_slug": self.kwargs["dealer_slug"], "entity_slug": self.kwargs["entity_slug"], "po_pk": po_model.uuid, }, ) redirect_url += f"?item_uuids={item_uuids}" return HttpResponseRedirect(redirect_url) for itemtxs in itemtxs_list: if not itemtxs.po_model_id: itemtxs.po_model_id = po_model.uuid itemtxs.clean() itemtxs_list = itemtxs_formset.save() po_model.update_state() po_model.clean() po_model.save( update_fields=["po_amount", "po_amount_received", "updated"] ) # if valid get saved formset from DB messages.add_message( request, messages.SUCCESS, "PO items updated successfully." ) return self.render_to_response(context=self.get_context_data()) # if not valid, return formset with errors... return self.render_to_response( context=self.get_context_data(itemtxs_formset=itemtxs_formset) ) return super(PurchaseOrderModelUpdateView, self).post( request, dealer_slug, entity_slug, *args, **kwargs ) def get_form(self, form_class=None): po_model: PurchaseOrderModel = self.object dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) if po_model.is_draft(): return DraftPurchaseOrderModelUpdateForm( entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin, **self.get_form_kwargs(), ) elif po_model.is_review(): return ReviewPurchaseOrderModelUpdateForm( entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin, **self.get_form_kwargs(), ) elif po_model.is_approved(): return ApprovedPurchaseOrderModelUpdateForm( entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin, **self.get_form_kwargs(), ) return BasePurchaseOrderModelUpdateForm( entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin, **self.get_form_kwargs(), ) def get_form_kwargs(self): if self.action_update_items: return { 'initial': self.get_initial(), 'prefix': self.get_prefix(), 'instance': self.object } return super(PurchaseOrderModelUpdateView, self).get_form_kwargs() def get_po_itemtxs_qs(self, po_model: PurchaseOrderModel): return po_model.itemtransactionmodel_set.select_related('bill_model', 'po_model').order_by('created') def form_valid(self, form: BasePurchaseOrderModelUpdateForm): po_model: PurchaseOrderModel = form.save(commit=False) if form.has_changed(): po_items_qs = ItemTransactionModel.objects.for_po( entity_slug=self.kwargs['entity_slug'], user_model=dealer.entity.admin, po_pk=po_model.uuid, ).select_related('bill_model') if all(['po_status' in form.changed_data, po_model.po_status == po_model.PO_STATUS_APPROVED]): po_items_qs.update(po_item_status=ItemTransactionModel.STATUS_NOT_ORDERED) if 'fulfilled' in form.changed_data: if not all([i.bill_model for i in po_items_qs]): messages.add_message(self.request, messages.ERROR, f'All PO items must be billed before marking' f' PO: {po_model.po_number} as fulfilled.', extra_tags='is-danger') return self.get(self.request) else: if not all([i.bill_model.is_paid() for i in po_items_qs]): messages.add_message(self.request, messages.SUCCESS, f'All bills must be paid before marking' f' PO: {po_model.po_number} as fulfilled.', extra_tags='is-success') return self.get(self.request) po_items_qs.update(po_item_status=ItemTransactionModel.STATUS_RECEIVED) messages.add_message(self.request, messages.SUCCESS, f'{self.object.po_number} successfully updated.', extra_tags='is-success') return super().form_valid(form) class BasePurchaseOrderActionActionView(LoginRequiredMixin, PermissionRequiredMixin, RedirectView, SingleObjectMixin): http_method_names = ['get'] pk_url_kwarg = 'po_pk' action_name = None commit = True permission_required = None queryset = None def get_queryset(self): dealer = get_object_or_404(Dealer, slug=self.kwargs['dealer_slug']) if self.queryset is None: self.queryset = PurchaseOrderModel.objects.for_entity( entity_slug=self.kwargs['entity_slug'], user_model=dealer.entity.admin ).select_related('entity', 'ce_model') return super().get_queryset() def get_redirect_url(self, dealer_slug, entity_slug, po_pk, *args, **kwargs): return reverse( "purchase_order_update", kwargs={ "dealer_slug": dealer_slug, "entity_slug": entity_slug, "po_pk": po_pk, }, ) def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs): # kwargs["user_model"] = dealer.entity.admin # Get user information for logging user_username = request.user.username if request.user.is_authenticated else 'anonymous' dealer = get_object_or_404(Dealer, slug=dealer_slug) kwargs["user_model"] = dealer.entity.admin if not self.action_name: raise ImproperlyConfigured("View attribute action_name is required.") response = super(BasePurchaseOrderActionActionView, self).get( request, dealer_slug, entity_slug, po_pk, *args, **kwargs ) po_model: PurchaseOrderModel = self.get_object() # Log the attempt to perform the action logger.debug( f"User {user_username} attempting to call action '{self.action_name}' " f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})." ) try: getattr(po_model, self.action_name)(commit=self.commit, **kwargs) # --- Single-line log for successful action --- logger.info( f"User {user_username} successfully executed action '{self.action_name}' " f"on Purchase Order ID: {po_model.pk}." ) messages.add_message( request, message="PO updated successfully.", level=messages.SUCCESS, ) except ValidationError as e: # --- Single-line log for ValidationError --- logger.warning( f"User {user_username} encountered a validation error " f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " f"Error: {e}" ) print(e) return response class BillModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): slug_url_kwarg = 'bill_pk' slug_field = 'uuid' context_object_name = 'bill' template_name = "bill/bill_detail.html" extra_context = { 'header_subtitle_icon': 'uil:bill', 'hide_menu': True } def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context["dealer"] = self.request.dealer bill_model: BillModel = self.object title = f'Bill {bill_model.bill_number}' context['page_title'] = title context['header_title'] = title bill_model: BillModel = self.object bill_items_qs, item_data = bill_model.get_itemtxs_data() context['itemtxs_qs'] = bill_items_qs context['total_amount__sum'] = item_data['total_amount__sum'] if not bill_model.is_configured(): link = format_html(f""" here """) msg = f'Bill {bill_model.bill_number} has not been fully set up. ' + \ f'Please update or assign associated accounts {link}.' messages.add_message(self.request, message=msg, level=messages.WARNING, extra_tags='is-danger') return context def get_queryset(self): dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug']) if self.queryset is None: entity_model = dealer.entity qs = entity_model.get_bills() self.queryset = qs return super().get_queryset() ######################################################3 #BILL class BillModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): slug_url_kwarg = 'bill_pk' slug_field = 'uuid' context_object_name = 'bill_model' template_name = "bill/bill_update.html" extra_context = { 'header_subtitle_icon': 'uil:bill' } http_method_names = ['get', 'post'] action_update_items = False queryset = None def get_queryset(self): dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug']) if self.queryset is None: entity_model = dealer.entity qs = entity_model.get_bills() self.queryset = qs return super().get_queryset().select_related( 'ledger', 'ledger__entity', 'vendor', 'cash_account', 'prepaid_account', 'unearned_account', 'cash_account__coa_model', 'prepaid_account__coa_model', 'unearned_account__coa_model' ) def get_form(self, form_class=None): form_class = self.get_form_class() dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug']) entity_model = dealer.entity if self.request.method == 'POST' and self.action_update_items: return form_class( entity_model=entity_model, user_model=dealer.entity.admin, instance=self.object ) return form_class( entity_model=entity_model, user_model=dealer.entity.admin, **self.get_form_kwargs() ) def get_form_class(self): bill_model: BillModel = self.object if not bill_model.is_configured(): return BillModelConfigureForm if bill_model.is_draft(): return DraftBillModelUpdateForm elif bill_model.is_review(): return InReviewBillModelUpdateForm elif bill_model.is_approved() and not bill_model.accrue: return ApprovedBillModelUpdateForm elif bill_model.is_approved() and bill_model.accrue: return AccruedAndApprovedBillModelUpdateForm elif bill_model.is_paid(): return PaidBillModelUpdateForm return BaseBillModelUpdateForm def get_context_data(self, *, object_list=None, itemtxs_formset=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug']) entity_model = dealer.entity bill_model: BillModel = self.object ledger_model = bill_model.ledger title = f'Bill {bill_model.bill_number}' context['page_title'] = title context['header_title'] = title context['header_subtitle'] = bill_model.get_bill_status_display() if not bill_model.is_configured(): messages.add_message( request=self.request, message=f'Bill {bill_model.bill_number} must have all accounts configured.', level=messages.ERROR, extra_tags='is-danger' ) if not bill_model.is_paid(): if ledger_model.locked: messages.add_message(self.request, messages.ERROR, f'Warning! This bill is locked. Must unlock before making any changes.', extra_tags='is-danger') if ledger_model.locked: messages.add_message(self.request, messages.ERROR, f'Warning! This bill is locked. Must unlock before making any changes.', extra_tags='is-danger') if not ledger_model.is_posted(): messages.add_message(self.request, messages.INFO, f'This bill has not been posted. Must post to see ledger changes.', extra_tags='is-info') itemtxs_qs = itemtxs_formset.get_queryset() if itemtxs_formset else None if not itemtxs_formset: itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model) itemtxs_formset = itemtxs_formset_class(entity_model=entity_model, bill_model=bill_model) itemtxs_qs, itemtxs_agg = bill_model.get_itemtxs_data(queryset=itemtxs_qs) has_po = any(i.po_model_id for i in itemtxs_qs) if has_po: itemtxs_formset.can_delete = False itemtxs_formset.has_po = has_po context['itemtxs_formset'] = itemtxs_formset context['total_amount__sum'] = itemtxs_agg['total_amount__sum'] context['has_po'] = has_po return context def get_success_url(self): return reverse( "bill-update", kwargs={ "dealer_slug": self.kwargs["dealer_slug"], "entity_slug": self.kwargs["entity_slug"], "bill_pk": self.kwargs["bill_pk"], }, ) def form_valid(self, form): form.save(commit=False) messages.add_message(self.request, messages.SUCCESS, f'Bill {self.object.bill_number} successfully updated.', extra_tags='is-success') return super().form_valid(form) def get(self, request,dealer_slug,entity_slug,bill_pk, *args, **kwargs): if self.action_update_items: return HttpResponseRedirect( redirect_to=reverse('bill-update', kwargs={ 'dealer_slug': dealer_slug, 'entity_slug': entity_slug, 'bill_pk': bill_pk }) ) return super(BillModelUpdateView, self).get(request, *args, **kwargs) def post(self, request, dealer_slug, entity_slug, bill_pk, *args, **kwargs): if self.action_update_items: if not request.user.is_authenticated: return HttpResponseForbidden() queryset = self.get_queryset() dealer = get_object_or_404(Dealer, slug=dealer_slug) entity_model = dealer.entity bill_model: BillModel = self.get_object(queryset=queryset) bill_pk = bill_model.uuid self.object = bill_model bill_itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model) itemtxs_formset = bill_itemtxs_formset_class( request.POST, bill_model=bill_model, entity_model=entity_model ) if itemtxs_formset.has_changed(): if itemtxs_formset.is_valid(): itemtxs_list = itemtxs_formset.save(commit=False) for itemtxs in itemtxs_list: itemtxs.bill_model_id = bill_model.uuid itemtxs.clean() itemtxs_formset.save() itemtxs_qs = bill_model.update_amount_due() bill_model.get_state(commit=True) bill_model.clean() bill_model.save( update_fields=[ "amount_due", "amount_receivable", "amount_unearned", "amount_earned", "updated", ] ) bill_model.migrate_state( entity_slug=self.kwargs["entity_slug"], user_model=self.request.user, itemtxs_qs=itemtxs_qs, raise_exception=False, ) messages.add_message( request, message=f"Items for Invoice {bill_model.bill_number} saved.", level=messages.SUCCESS, ) # if valid get saved formset from DB return HttpResponseRedirect( redirect_to=reverse( "bill-update", kwargs={ "dealer_slug": dealer_slug, "entity_slug": entity_model.slug, "bill_pk": bill_pk, }, ) ) context = self.get_context_data(itemtxs_formset=itemtxs_formset) return self.render_to_response(context=context) return super(BillModelUpdateView, self).post( request, dealer_slug, entity_slug, bill_pk, **kwargs ) class BaseBillActionView(LoginRequiredMixin,PermissionRequiredMixin, RedirectView, SingleObjectMixin): http_method_names = ['get'] pk_url_kwarg = 'bill_pk' action_name = None commit = True permission_required = "django_ledger.change_billmodel" queryset = None def get_queryset(self): if self.queryset is None: dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) entity_model = dealer.entity qs = entity_model.get_bills() self.queryset = qs return super().get_queryset() def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs): return reverse( "bill-update", kwargs={ "dealer_slug": dealer_slug, "entity_slug": entity_slug, "bill_pk": bill_pk, }, ) def get(self, request, *args, **kwargs): dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) kwargs['user_model'] = dealer.entity.admin if not self.action_name: raise ImproperlyConfigured('View attribute action_name is required.') response = super(BaseBillActionView, self).get(request, *args, **kwargs) bill_model: BillModel = self.get_object() try: getattr(bill_model, self.action_name)(commit=self.commit, **kwargs) except ValidationError as e: messages.add_message(request, message=e.message, level=messages.ERROR, extra_tags='is-danger') return response