diff --git a/.gitignore b/.gitignore index 5424089d..237f8bef 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ wheels/ *.egg *.manifest *.spec +inventory/management/commands/run.py # Installer logs pip-log.txt 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 eda0c31a..23f17d27 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) @@ -477,7 +478,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" @@ -486,27 +487,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): @@ -682,7 +685,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): @@ -1888,25 +1893,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}' - 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 @@ -1921,10 +1936,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) @@ -2038,7 +2054,7 @@ def create_estimate(request): { "item_number": item_instance.item_number, "quantity": Decimal(item.get("quantity")), - "unit_cost": car_instance.finances.cost_price, + "unit_cost": car_instance.finances.selling_price, "unit_revenue": car_instance.finances.selling_price, "total_amount": (car_instance.finances.total_vat) * int(item.get("quantity")), @@ -2206,7 +2222,6 @@ class InvoiceListView(LoginRequiredMixin, ListView): model = InvoiceModel template_name = "sales/invoices/invoice_list.html" context_object_name = "invoices" - paginate_by = 10 def get_queryset(self): dealer = get_user_type(self.request) @@ -2300,28 +2315,17 @@ class PaidInvoiceModelUpdateFormView(LoginRequiredMixin, UpdateView): @login_required def invoice_mark_as(request, pk): invoice = get_object_or_404(InvoiceModel, pk=pk) - entity = EntityModel.objects.first() # will change later - user = entity.admin + dealer = get_user_type(request) mark = request.GET.get("mark") - if mark: - if mark == "accept": - 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=entity.slug, user_model=user) - # invoice.post_ledger() - invoice.save() - ledger = ( - entity.get_ledgers().filter(name=f"Invoice {str(invoice.pk)}").first() - ) - if not ledger: - ledger = entity.create_ledger(name=f"Invoice {str(invoice.pk)}") - ledger.invoicemodel = invoice - ledger.save() - # elif mark == "complete": - # if not invoice.can_complete(): - # messages.error(request, "invoice is not ready for completion") + if mark and mark == "accept": + 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() + return redirect("invoice_detail", pk=invoice.pk) def invoice_create(request, pk): @@ -2342,18 +2346,28 @@ def invoice_create(request, pk): invoice.save() unit_items = estimate.get_itemtxs_data()[0] + vat = models.VatRate.objects.filter(is_active=True).first() + total = 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 + + grand_total = Decimal(total) - Decimal(discount_amount) + vat_amount = round(Decimal(grand_total) * Decimal(vat.rate), 2) + grand_total += Decimal(vat_amount) + unit_cost = grand_total / Decimal(item.ce_quantity) itemtxs.append( { "item_number": item.item_model.item_number, - "unit_cost": car.finances.total_vat, - "unit_revenue": car.finances.total_vat, + "unit_cost": unit_cost, + "unit_revenue": unit_cost, "quantity": item.ce_quantity, - "total_amount": Decimal(car.finances.total_vat) - * Decimal(item.ce_quantity), + "total_amount": grand_total, } ) invoice_itemtxs = { @@ -2434,8 +2448,6 @@ def PaymentCreateView(request, pk=None): amount = form.cleaned_data.get("amount") invoice = form.cleaned_data.get("invoice") payment_method = form.cleaned_data.get("payment_method") - - ledger = None try: vat_amount = 0 total_amount = 0 @@ -2444,10 +2456,10 @@ def PaymentCreateView(request, pk=None): for x in invoice.get_itemtxs_data()[0].all(): vat_amount += models.Car.objects.get( vin=x.item_model.name - ).finances.vat_amount - total_amount += models.Car.objects.get( - vin=x.item_model.name - ).finances.total_discount + ).finances.vat_amount * Decimal(x.quantity) + total_amount += Decimal(x.unit_cost) * Decimal(x.quantity) + + grand_total = total_amount - Decimal(vat_amount) ledger = LedgerModel.objects.filter( name=str(invoice.pk), entity=entity @@ -2490,7 +2502,7 @@ def PaymentCreateView(request, pk=None): TransactionModel.objects.create( journal_entry=journal, account=credit_account, # Credit Accounts Receivable - amount=total_amount, # Payment amount + amount=grand_total, # Payment amount tx_type="credit", description="Payment Received", ) @@ -2614,9 +2626,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 @@ -2626,9 +2638,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) @@ -2636,8 +2650,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 @@ -2789,7 +2804,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" @@ -2804,7 +2819,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" @@ -2880,6 +2896,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 %} diff --git a/templates/ledger/coa_accounts/account_detail.html b/templates/ledger/coa_accounts/account_detail.html index 48674f76..07798133 100644 --- a/templates/ledger/coa_accounts/account_detail.html +++ b/templates/ledger/coa_accounts/account_detail.html @@ -1,137 +1,122 @@ -{% extends "base.html" %} +{% extends 'base.html' %} {% load i18n %} -{% block title %}{{ page_title }}{% endblock title %} +{% block title %} + {{ page_title }} +{% endblock %} {% block content %} - -