diff --git a/inventory/urls.py b/inventory/urls.py index 0b12fcc3..08b832fe 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -179,6 +179,10 @@ urlpatterns = [ path('items/expeneses/', views.ItemExpenseListView.as_view(), name='item_expense_list'), path('items/expeneses/create/', views.ItemExpenseCreateView.as_view(), name='item_expense_create'), path('items/expeneses//update/', views.ItemExpenseUpdateView.as_view(), name='item_expense_update'), + # Bills + path('items/bills/', views.BillListView.as_view(), name='bill_list'), + path('items/bills/create/', views.BillCreateView.as_view(), name='bill_create'), + # path('items/bills//update/', views.ItemExpenseUpdateView.as_view(), name='item_expense_update'), ] diff --git a/inventory/views.py b/inventory/views.py index b8b5db5b..b7565056 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -14,11 +14,13 @@ from django_ledger.models import ( CustomerModel, LedgerModel, ItemModel, + BillModel ) from django_ledger.forms.bank_account import ( BankAccountCreateForm, BankAccountUpdateForm, ) +from django_ledger.forms.bill import BillModelCreateForm from django_ledger.forms.invoice import ( DraftInvoiceModelUpdateForm, ApprovedInvoiceModelUpdateForm, @@ -89,7 +91,6 @@ import numpy as np from pyzbar.pyzbar import decode - logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -439,7 +440,7 @@ class AjaxHandlerView(LoginRequiredMixin, View): return JsonResponse(serialized_options, safe=False) -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class SearchCodeView(View): template_name = "inventory/scan_vin.html" @@ -448,27 +449,29 @@ class SearchCodeView(View): return render(request, self.template_name) def post(self, request, *args, **kwargs): - image_file = request.FILES.get('image') + image_file = request.FILES.get("image") if image_file: print("image received!") - image = cv2.imdecode(np.frombuffer(image_file.read(), np.uint8), cv2.IMREAD_COLOR) + image = cv2.imdecode( + np.frombuffer(image_file.read(), np.uint8), cv2.IMREAD_COLOR + ) decoded_objects = decode(image) if decoded_objects: print("image decoded!") print(decoded_objects[0]) - code = decoded_objects[0].data.decode('utf-8') + code = decoded_objects[0].data.decode("utf-8") print("code received!") print(code) car = get_object_or_404(models.Car, vin=code) name = car.id_car_make.get_local_name print(name) - return redirect('car_detail', pk=car.pk) + return redirect("car_detail", pk=car.pk) else: print("back to else statement") - return JsonResponse({'success': False, 'error': 'No code detected'}) + return JsonResponse({"success": False, "error": "No code detected"}) else: - return JsonResponse({'success': False, 'error': 'No image provided'}) + return JsonResponse({"success": False, "error": "No image provided"}) class CarInventory(LoginRequiredMixin, ListView): @@ -638,7 +641,9 @@ class CarFinanceCreateView(LoginRequiredMixin, CreateView): def get_form(self, form_class=None): form = super().get_form(form_class) dealer = get_user_type(self.request) - form.fields["additional_finances"].queryset = models.AdditionalServices.objects.filter(dealer=dealer) + form.fields[ + "additional_finances" + ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) return form # def get_initial(self): @@ -1844,27 +1849,35 @@ class AccountDetailView(LoginRequiredMixin, DetailView): model = AccountModel template_name = "ledger/coa_accounts/account_detail.html" context_object_name = "account" - slug_field = 'uuid' + slug_field = "uuid" DEFAULT_TXS_DAYS = 30 extra_context = { - 'DEFAULT_TXS_DAYS': DEFAULT_TXS_DAYS, - 'header_subtitle_icon': 'ic:round-account-tree' + "DEFAULT_TXS_DAYS": DEFAULT_TXS_DAYS, + "header_subtitle_icon": "ic:round-account-tree", } def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - account_model: AccountModel = context['object'] - context['header_title'] = f'Account {account_model.code} - {account_model.name}' - context['page_title'] = f'Account {account_model.code} - {account_model.name}' - context['total_debits'] = sum(x.amount for x in account_model.transactionmodel_set.filter(tx_type='debit')) - context['total_credits'] = sum(x.amount for x in account_model.transactionmodel_set.filter(tx_type='credit')) - txs_qs = account_model.transactionmodel_set.all().posted().order_by( - 'journal_entry__timestamp' - ).select_related( - 'journal_entry', - 'journal_entry__entity_unit', - 'journal_entry__ledger__billmodel', - 'journal_entry__ledger__invoicemodel', + account_model: AccountModel = context["object"] + context["header_title"] = f"Account {account_model.code} - {account_model.name}" + context["page_title"] = f"Account {account_model.code} - {account_model.name}" + context["total_debits"] = sum( + x.amount for x in account_model.transactionmodel_set.filter(tx_type="debit") + ) + context["total_credits"] = sum( + x.amount + for x in account_model.transactionmodel_set.filter(tx_type="credit") + ) + txs_qs = ( + account_model.transactionmodel_set.all() + .posted() + .order_by("journal_entry__timestamp") + .select_related( + "journal_entry", + "journal_entry__entity_unit", + "journal_entry__ledger__billmodel", + "journal_entry__ledger__invoicemodel", + ) ) return context @@ -1879,10 +1892,11 @@ class AccountUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): def get_form(self, form_class=None): form = super().get_form(form_class) - form.fields['_ref_node_id'].widget = HiddenInput() - form.fields['_position'].widget = HiddenInput() + form.fields["_ref_node_id"].widget = HiddenInput() + form.fields["_position"].widget = HiddenInput() return form + @login_required def account_delete(request, pk): account = get_object_or_404(AccountModel, pk=pk) @@ -2262,8 +2276,10 @@ def invoice_mark_as(request, pk): if not invoice.can_approve(): messages.error(request, "invoice is not ready for approval") return redirect("invoice_detail", pk=invoice.pk) - invoice.mark_as_approved(entity_slug=dealer.entity.slug, user_model=dealer.entity.admin) - invoice.save() + invoice.mark_as_approved( + entity_slug=dealer.entity.slug, user_model=dealer.entity.admin + ) + invoice.save() return redirect("invoice_detail", pk=invoice.pk) @@ -2285,17 +2301,17 @@ def invoice_create(request, pk): invoice.save() unit_items = estimate.get_itemtxs_data()[0] - vat = models.VatRate.objects.filter(is_active=True).first() + vat = models.VatRate.objects.filter(is_active=True).first() total = 0 - discount_amount = 0 - + discount_amount = 0 + itemtxs = [] - for item in unit_items: - car = models.Car.objects.get(vin=item.item_model.name) - - total = Decimal(car.finances.total) * Decimal(item.ce_quantity) - discount_amount = car.finances.discount_amount - + for item in unit_items: + car = models.Car.objects.get(vin=item.item_model.name) + + total = Decimal(car.finances.total) * Decimal(item.ce_quantity) + discount_amount = car.finances.discount_amount + grand_total = Decimal(total) - Decimal(discount_amount) vat_amount = round(Decimal(grand_total) * Decimal(vat.rate), 2) grand_total += Decimal(vat_amount) @@ -2306,9 +2322,9 @@ def invoice_create(request, pk): "unit_cost": unit_cost, "unit_revenue": unit_cost, "quantity": item.ce_quantity, - "total_amount": grand_total + "total_amount": grand_total, } - ) + ) invoice_itemtxs = { i.get("item_number"): { "unit_cost": i.get("unit_cost"), @@ -2386,7 +2402,7 @@ def PaymentCreateView(request, pk=None): if form.is_valid(): amount = form.cleaned_data.get("amount") invoice = form.cleaned_data.get("invoice") - payment_method = form.cleaned_data.get("payment_method") + payment_method = form.cleaned_data.get("payment_method") try: vat_amount = 0 total_amount = 0 @@ -2567,9 +2583,9 @@ class LeadDetailView(DetailView): class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin): model = models.Lead form_class = forms.LeadForm - template_name = 'crm/leads/lead_form.html' + template_name = "crm/leads/lead_form.html" # success_message = "Lead created successfully!" - success_url = reverse_lazy('lead_list') + success_url = reverse_lazy("lead_list") def form_valid(self, form): print("Form data:", form.cleaned_data) # Debug form data @@ -2579,9 +2595,11 @@ class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin): def get_car_models(request): - make_id = request.GET.get('id_car_make') + make_id = request.GET.get("id_car_make") if make_id: - car_models = models.CarModel.objects.filter(id_car_make=make_id).values('id_car_model', 'name', 'arabic_name') + car_models = models.CarModel.objects.filter(id_car_make=make_id).values( + "id_car_model", "name", "arabic_name" + ) return JsonResponse(list(car_models), safe=False) return JsonResponse([], safe=False) @@ -2589,8 +2607,9 @@ def get_car_models(request): class LeadUpdateView(UpdateView): model = models.Lead form_class = forms.LeadForm - template_name = 'crm/leads/lead_form.html' - success_url = reverse_lazy('lead_list') + template_name = "crm/leads/lead_form.html" + success_url = reverse_lazy("lead_list") + class LeadDeleteView(DeleteView): model = models.Lead @@ -2742,7 +2761,7 @@ def fetch_notifications(request): return JsonResponse({"notifications": notifications_data}) -class ItemServiceCreateView(LoginRequiredMixin,SuccessMessageMixin,CreateView): +class ItemServiceCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = models.AdditionalServices form_class = forms.AdditionalServiceForm template_name = "items/service/service_create.html" @@ -2757,7 +2776,8 @@ class ItemServiceCreateView(LoginRequiredMixin,SuccessMessageMixin,CreateView): form.instance.price = (form.instance.price * vat.rate) + form.instance.price return super().form_valid(form) -class ItemServiceUpdateView(LoginRequiredMixin,SuccessMessageMixin,UpdateView): + +class ItemServiceUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = models.AdditionalServices form_class = forms.AdditionalServiceForm template_name = "items/service/service_create.html" @@ -2833,6 +2853,36 @@ class ItemExpenseListView(ListView): return items +class BillListView(ListView): + model = ItemModel + template_name = "ledger/bills/bill_list.html" + context_object_name = "bills" + + def get_queryset(self): + dealer = get_user_type(self.request) + items = dealer.entity.get_bills() + return items + + +class BillCreateView(LoginRequiredMixin,SuccessMessageMixin,CreateView): + model = BillModel + form_class = BillModelCreateForm + template_name = "ledger/bills/bill_form.html" + success_url = reverse_lazy("bill_list") + success_message = _("Bill created successfully.") + + def get_form_kwargs(self): + dealer = get_user_type(self.request) + kwargs = super().get_form_kwargs() + kwargs["entity_model"] = dealer.entity + return kwargs + + # def form_valid(self, form): + # dealer = get_user_type(self.request) + # form.instance.entity = dealer.entity + # return super().form_valid(form) + + class SubscriptionPlans(ListView): model = models.SubscriptionPlan template_name = "subscriptions/subscription_plan.html" diff --git a/templates/ledger/bills/bill_detail.html b/templates/ledger/bills/bill_detail.html new file mode 100644 index 00000000..07798133 --- /dev/null +++ b/templates/ledger/bills/bill_detail.html @@ -0,0 +1,122 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %} + {{ page_title }} +{% endblock %} + +{% block content %} + + + +
+
+
+

{{ header_title|upper }}

+
+
+
+
+

+ {{ _('Account Name') }}: {{ account.name }} +

+

+ {{ _('Account Code') }}: {{ account.code }} +

+
+
+

+ {{ _('Balance Type') }}: {{ account.balance_type }} +

+

+ {{ _('Active') }}: {{ account.active }} +

+
+
+
+
+ + + + + + + + + + + + {% for tx in account.transactionmodel_set.all %} + + + + + + + + + + {% endfor %} + + + + + + + + + +
{{ _('JE Number') }}{{ _('Date') }}{{ _('Debit') }}{{ _('Credit') }}{{ _('Description') }}{{ _('Unit') }}{{ _('Actions') }}
{{ tx.journal_entry.je_number }}{{ tx.journal_entry.timestamp }} + {% if tx.tx_type == 'debit' %} + ${{ tx.amount }} + {% endif %} + + {% if tx.tx_type == 'credit' %} + ${{ tx.amount }} + {% endif %} + {{ tx.description }}{{ tx.journal_entry.entity_unit.name }} + +
Total${{ total_debits }}${{ total_credits }}
+
+
+
+ +
+
+{% endblock %} diff --git a/templates/ledger/bills/bill_form.html b/templates/ledger/bills/bill_form.html new file mode 100644 index 00000000..1e9759c7 --- /dev/null +++ b/templates/ledger/bills/bill_form.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% load i18n %} +{% load crispy_forms_filters %} +{% block title %}{% trans "account" %}{% endblock title %} + +{% block content %} +
+ +
+
+

+ {% if account.created %} + + {{ _("Edit Account") }} + {% else %} + + {{ _("Add Account") }} + {% endif %} +

+
+
+
+ {% csrf_token %} + {{ form|crispy }} + {% for error in form.errors %} +
{{ error }}
+ {% endfor %} +
+ + {% trans "Cancel"|capfirst %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/ledger/bills/bill_list.html b/templates/ledger/bills/bill_list.html new file mode 100644 index 00000000..a7830700 --- /dev/null +++ b/templates/ledger/bills/bill_list.html @@ -0,0 +1,165 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %} + {% trans 'Bills' %} +{% endblock %} +{% block bills %} + + {% trans 'Bills'|capfirst %} + (current) + +{% endblock %} +{% block content %} +
+
+
+ +
+
+
+
+
+ + + {% if request.GET.q %} + + {% endif %} +
+
+
+
+
+ + + + {% if page_obj.object_list %} +
+
+ + + + + + + + + + + + {% for bill in bills %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
+ {% trans 'Account Name' %} + + {% trans 'Code' %} + + {% trans 'Balance Type' %} + + {% trans 'Active' %} +
{{ bill.name }}{{ bill.code }} + {% if bill.balance_type == 'debit' %} +
+ {{ _('Debit') }} +
+ {% else %} +
+ {{ _('Credit') }} +
+ {% endif %} +
+ {% if bill.active %} + + {% else %} + + {% endif %} + +
+ + +
+
+ {% trans 'No bill found.' %} +
+
+ +
+ {{ page_obj.start_index }} {{ _('to') }} {{ page_obj.end_index }}{{ _('Items of') }}{{ page_obj.paginator.count }} +
+ +
+
+
+ {% endif %} +
+
+
+{% endblock %}