diff --git a/.DS_Store b/.DS_Store index 142e5f91..4e9a6eca 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 237f8bef..71c44dd4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ db.sqlite db.sqlite3 media car_inventory/settings.py +scripts/dsrpipe.py # Backup files # *.bak diff --git a/api/__pycache__/serializers.cpython-311.pyc b/api/__pycache__/serializers.cpython-311.pyc index 9e450747..b4949161 100644 Binary files a/api/__pycache__/serializers.cpython-311.pyc and b/api/__pycache__/serializers.cpython-311.pyc differ diff --git a/api/__pycache__/urls.cpython-311.pyc b/api/__pycache__/urls.cpython-311.pyc index b84ced86..2e159340 100644 Binary files a/api/__pycache__/urls.cpython-311.pyc and b/api/__pycache__/urls.cpython-311.pyc differ diff --git a/api/__pycache__/views.cpython-311.pyc b/api/__pycache__/views.cpython-311.pyc index 85e62eba..670f4d26 100644 Binary files a/api/__pycache__/views.cpython-311.pyc and b/api/__pycache__/views.cpython-311.pyc differ diff --git a/api/routing.py b/api/routing.py index 895c23d3..93ee2d61 100644 --- a/api/routing.py +++ b/api/routing.py @@ -3,4 +3,5 @@ # # websocket_urlpatterns = [ # re_path(r'ws/vin_scan/$', consumers.VINScanConsumer.as_asgi()), -# ] \ No newline at end of file +# ] + diff --git a/api/serializers.py b/api/serializers.py index 82e82912..80eba627 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,5 +1,6 @@ -from rest_framework import serializers from . import models +from rest_framework import serializers +from inventory import models as inventory_models class CarVINSerializer(serializers.ModelSerializer): @@ -12,3 +13,71 @@ class CarVINSerializer(serializers.ModelSerializer): return models.CarVIN.objects.create(vin=vin, **validated_data) +class CarMakeSerializer(serializers.ModelSerializer): + car_models = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carmodel_set') + + class Meta: + model = inventory_models.CarMake + fields = '__all__' + + +class CarModelSerializer(serializers.ModelSerializer): + car_series = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carserie_set') + + class Meta: + model = inventory_models.CarModel + fields = '__all__' + + +class CarSerieSerializer(serializers.ModelSerializer): + car_trims = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='cartrim_set') + + class Meta: + model = inventory_models.CarSerie + fields = '__all__' + + +class CarTrimSerializer(serializers.ModelSerializer): + car_equipments = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carequipment_set') + car_specification_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True, + source='carspecificationvalue_set') + + class Meta: + model = inventory_models.CarTrim + fields = '__all__' + + +class CarEquipmentSerializer(serializers.ModelSerializer): + car_option_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='caroptionvalue_set') + + class Meta: + model = inventory_models.CarEquipment + fields = '__all__' + + +class CarSpecificationSerializer(serializers.ModelSerializer): + child_specifications = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carspecification_set') + + class Meta: + model = inventory_models.CarSpecification + fields = '__all__' + + +class CarSpecificationValueSerializer(serializers.ModelSerializer): + class Meta: + model = inventory_models.CarSpecificationValue + fields = '__all__' + + +class CarOptionSerializer(serializers.ModelSerializer): + child_options = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='caroption_set') + + class Meta: + model = inventory_models.CarOption + fields = '__all__' + + +class CarOptionValueSerializer(serializers.ModelSerializer): + class Meta: + model = inventory_models.CarOptionValue + fields = '__all__' \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index ae254510..a5713d5e 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,8 +1,22 @@ -from django.urls import path +from django.urls import path, include +from rest_framework import routers + from api import views +router = routers.DefaultRouter() +router.register(r'car-makes', views.CarMakeViewSet) +router.register(r'car-models', views.CarModelViewSet) +router.register(r'car-series', views.CarSerieViewSet) +router.register(r'car-trims', views.CarTrimViewSet) +router.register(r'car-equipments', views.CarEquipmentViewSet) +router.register(r'car-specifications', views.CarSpecificationViewSet) +router.register(r'car-specification-values', views.CarSpecificationValueViewSet) +router.register(r'car-options', views.CarOptionViewSet) +router.register(r'car-option-values', views.CarOptionValueViewSet) + urlpatterns = [ + path('', include(router.urls)), path('cars/vin/', views.CarVINViewSet.as_view(), name='car_vin'), path('login/', views.LoginView.as_view(), name='login'), - ] +] diff --git a/api/views.py b/api/views.py index 538a6175..d0c03086 100644 --- a/api/views.py +++ b/api/views.py @@ -1,5 +1,6 @@ +from django.db.models import Q from django.views.decorators.csrf import csrf_exempt -from rest_framework import permissions, status, viewsets +from rest_framework import permissions, status, viewsets, generics from rest_framework.views import APIView from rest_framework.response import Response from django.contrib.auth import authenticate @@ -7,8 +8,9 @@ from django.shortcuts import render from . import models, serializers from .services import get_car_data, get_from_cardatabase from rest_framework.authtoken.models import Token -# from inventory.models import CustomUser from django.utils.decorators import method_decorator +from inventory import models as inventory_models + class LoginView(APIView): @@ -51,3 +53,46 @@ class CarVINViewSet(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class CarMakeViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarMake.objects.filter(is_sa_import=True) + serializer_class = serializers.CarMakeSerializer + + +class CarModelViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarModel.objects.filter(id_car_make__is_sa_import=True) + serializer_class = serializers.CarModelSerializer + + +class CarSerieViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarSerie.objects.all() + serializer_class = serializers.CarSerieSerializer + + +class CarTrimViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarTrim.objects.all() + serializer_class = serializers.CarTrimSerializer + + +class CarEquipmentViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarEquipment.objects.all() + serializer_class = serializers.CarEquipmentSerializer + + +class CarSpecificationViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarSpecification.objects.all() + serializer_class = serializers.CarSpecificationSerializer + + +class CarSpecificationValueViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarSpecificationValue.objects.all() + serializer_class = serializers.CarSpecificationValueSerializer + + +class CarOptionViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarOption.objects.all() + serializer_class = serializers.CarOptionSerializer + + +class CarOptionValueViewSet(viewsets.ModelViewSet): + queryset = inventory_models.CarOptionValue.objects.all() + serializer_class = serializers.CarOptionValueSerializer diff --git a/car_inventory/__pycache__/settings.cpython-311.pyc b/car_inventory/__pycache__/settings.cpython-311.pyc index eeca0380..f8667ea2 100644 Binary files a/car_inventory/__pycache__/settings.cpython-311.pyc and b/car_inventory/__pycache__/settings.cpython-311.pyc differ diff --git a/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index bf9df5b5..ec71d055 100644 Binary files a/inventory/__pycache__/forms.cpython-311.pyc and b/inventory/__pycache__/forms.cpython-311.pyc differ diff --git a/inventory/__pycache__/models.cpython-311.pyc b/inventory/__pycache__/models.cpython-311.pyc index b6987177..40997027 100644 Binary files a/inventory/__pycache__/models.cpython-311.pyc and b/inventory/__pycache__/models.cpython-311.pyc differ diff --git a/inventory/__pycache__/urls.cpython-311.pyc b/inventory/__pycache__/urls.cpython-311.pyc index e5a202ad..64723f40 100644 Binary files a/inventory/__pycache__/urls.cpython-311.pyc and b/inventory/__pycache__/urls.cpython-311.pyc differ diff --git a/inventory/__pycache__/views.cpython-311.pyc b/inventory/__pycache__/views.cpython-311.pyc index b1e9ada4..e919829b 100644 Binary files a/inventory/__pycache__/views.cpython-311.pyc and b/inventory/__pycache__/views.cpython-311.pyc differ diff --git a/inventory/management/commands/__pycache__/translate.cpython-311.pyc b/inventory/management/commands/__pycache__/translate.cpython-311.pyc index cda4a7bd..d578167b 100644 Binary files a/inventory/management/commands/__pycache__/translate.cpython-311.pyc and b/inventory/management/commands/__pycache__/translate.cpython-311.pyc differ diff --git a/inventory/management/commands/translate.py b/inventory/management/commands/translate.py index 7bcd1b31..0ac7bd94 100644 --- a/inventory/management/commands/translate.py +++ b/inventory/management/commands/translate.py @@ -1,6 +1,6 @@ from openai import OpenAI from django.core.management.base import BaseCommand -from inventory.models import CarSerie, CarModel, CarMake, CarTrim, CarOption, CarSpecificationValue +from inventory.models import CarSerie, CarModel, CarMake, CarTrim, CarOption, CarSpecification from django.conf import settings @@ -9,28 +9,35 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): client = OpenAI(api_key=settings.OPENAI_API_KEY) - car_option = CarOption.objects.all()[10300:] - total = car_option.count() + en_value = CarModel.objects.all() + + total = en_value.count() print(f'Translating {total} names...') - for index, car_option in enumerate(car_option, start=1): - try: - completion = client.chat.completions.create( - model="gpt-4o", - messages=[ - { - "role": "system", - "content": "You are an assistant that translates English to Arabic." - }, - { - "role": "user", - "content": car_option.name - } - ], - temperature=0.2, - ) - translation = completion.choices[0].message.content.strip() - car_option.arabic_name = translation - car_option.save() - print(f"[{index}/{total}] Translated '{car_option.name}' to '{translation}'") - except Exception as e: - print(f"Error translating '{car_option.name}': {e}") + for index, en_value in enumerate(en_value, start=1): + if not en_value.arabic_name: + try: + completion = client.chat.completions.create( + model="gpt-4o", + messages=[ + { + "role": "system", + "content": ( + "You are an assistant that translates English to Arabic." + "You are an assistant specialized in cars and automotive terms." + "If the model name is a number just write it as is" + "You can get the arabic names for makes, models, series, trims, options, and specifications." + ) + }, + { + "role": "user", + "content": en_value.name + } + ], + temperature=0.2, + ) + translation = completion.choices[0].message.content.strip() + en_value.arabic_name = translation + en_value.save() + print(f"[{index}/{total}] .. Done") + except Exception as e: + print(f"Error translating '{en_value.name}': {e}") diff --git a/inventory/models.py b/inventory/models.py index f78ae7fd..d05bb82b 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -452,6 +452,10 @@ class Car(models.Model): "id": self.id, } + def get_specifications(self): + specs = CarSpecificationValue.objects.filter(id_car_trim=self.id_car_trim) + return specs + class CarTransfer(models.Model): car = models.ForeignKey( "Car", diff --git a/inventory/views.py b/inventory/views.py index dc91836e..44401c15 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -115,6 +115,28 @@ from django.core.files.storage import default_storage from plans.models import Plan,PlanPricing from django_ledger.utils import accruable_net_summary from appointment.views_admin import fetch_user_appointments +from django_ledger.views.financial_statement import ( + FiscalYearBalanceSheetView, + BaseIncomeStatementRedirectView, + FiscalYearIncomeStatementView, + BaseCashFlowStatementRedirectView, + FiscalYearCashFlowStatementView, +) +from django_ledger.views.entity import EntityModelDetailBaseView,EntityModelDetailHandlerView +from django.views.generic import DetailView, RedirectView + +from django_ledger.io.io_core import get_localdate +from django_ledger.models import EntityModel, EntityUnitModel +from django_ledger.views.mixins import ( + QuarterlyReportMixIn, + YearlyReportMixIn, + MonthlyReportMixIn, + DateReportMixIn, + DjangoLedgerSecurityMixIn, + EntityUnitMixIn, + BaseDateNavigationUrlMixIn, + PDFReportMixIn, +) logger = logging.getLogger(__name__) @@ -2649,10 +2671,11 @@ class PaymentRequest(LoginRequiredMixin, DetailView): models.Car.objects.get(vin=car.item_model.name) for car in context["estimate"].get_itemtxs_data()[0].all() ] + return context -class EstimatePreviewView(LoginRequiredMixin, DetailView): +class EstimatePreviewView(DetailView): model = EstimateModel context_object_name = "estimate" template_name = "sales/estimates/estimate_preview.html" @@ -2674,30 +2697,31 @@ class EstimatePreviewView(LoginRequiredMixin, DetailView): @login_required def estimate_mark_as(request, pk): estimate = get_object_or_404(EstimateModel, pk=pk) - entity = estimate.entity + dealer = get_user_type(request.user) + entity = dealer.entity mark = request.GET.get("mark") if mark: if mark == "review": if not estimate.can_review(): - messages.error(request, "Estimate is not ready for review") + messages.error(request, _("Estimate is not ready for review")) return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_review() elif mark == "approved": if not estimate.can_approve(): - messages.error(request, "Estimate is not ready for approval") + messages.error(request, _("Estimate is not ready for approval")) return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_approved() - messages.success(request, "Estimate approved successfully.") + messages.success(request, _("Estimate approved successfully.")) elif mark == "rejected": if not estimate.can_cancel(): - messages.error(request, "Estimate is not ready for rejection") + messages.error(request, _("Estimate is not ready for rejection")) return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_canceled() - messages.success(request, "Estimate canceled successfully.") + messages.success(request, _("Estimate canceled successfully.")) elif mark == "completed": if not estimate.can_complete(): - messages.error(request, "Estimate is not ready for completion") + messages.error(request, _("Estimate is not ready for completion")) return redirect("estimate_detail", pk=estimate.pk) estimate.save() messages.success(request, "Estimate marked as " + mark.upper()) @@ -2957,7 +2981,7 @@ def PaymentCreateView(request, pk): try: if invoice: set_invoice_payment(dealer, entity, invoice, amount, payment_method) - elif bill: + elif bill: set_bill_payment(dealer, entity, bill, amount, payment_method) messages.success(request, "Payment created successfully!") return redirect(redirect_url, pk=model.pk) @@ -3094,6 +3118,7 @@ def lead_create(request): return render(request, "crm/leads/lead_form.html", {"form": form}) + class LeadUpdateView(UpdateView): model = models.Lead form_class = forms.LeadForm @@ -3105,6 +3130,8 @@ class LeadUpdateView(UpdateView): form.fields["id_car_model"].queryset = form.instance.id_car_make.carmodel_set.all() return form + +@login_required def LeadDeleteView(request,pk): lead = get_object_or_404(models.Lead, pk=pk) lead.delete() @@ -3231,7 +3258,7 @@ def send_lead_email(request, pk): request.POST.get("subject"), request.POST.get("message"), ) - messages.success(request, "Email sent successfully!") + messages.success(request, _("Email sent successfully!")) return redirect("lead_list") msg = f""" السلام عليكم @@ -3278,7 +3305,7 @@ def add_activity_to_lead(request, pk): return render(request, "crm/add_activity.html", {"form": form, "lead": lead}) -class OpportunityCreateView(CreateView): +class OpportunityCreateView(CreateView, LoginRequiredMixin): model = models.Opportunity form_class = forms.OpportunityForm template_name = "crm/opportunities/opportunity_form.html" @@ -3309,7 +3336,7 @@ class OpportunityCreateView(CreateView): return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk}) -class OpportunityUpdateView(UpdateView): +class OpportunityUpdateView(UpdateView, LoginRequiredMixin): model = models.Opportunity form_class = forms.OpportunityForm template_name = "crm/opportunities/opportunity_form.html" @@ -3432,7 +3459,8 @@ class ItemServiceCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView) def form_valid(self, form): vat = models.VatRate.objects.get(is_active=True) - form.instance.dealer = get_user_type(self.request.user.dealer) + dealer = get_user_type(self.request) + form.instance.dealer = dealer if form.instance.taxable: form.instance.price = (form.instance.price * vat.rate) + form.instance.price return super().form_valid(form) @@ -3448,13 +3476,14 @@ class ItemServiceUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView) def form_valid(self, form): vat = models.VatRate.objects.get(is_active=True) - form.instance.dealer = get_user_type(self.request.user.dealer) + dealer = get_user_type(self.request) + form.instance.dealer = dealer if form.instance.taxable: form.instance.price = (form.instance.price * vat.rate) + form.instance.price return super().form_valid(form) -class ItemServiceListView(ListView): +class ItemServiceListView(ListView, LoginRequiredMixin): model = models.AdditionalServices template_name = "items/service/service_list.html" context_object_name = "services" @@ -3465,7 +3494,7 @@ class ItemServiceListView(ListView): return models.AdditionalServices.objects.filter(dealer=dealer).all() -class ItemExpenseCreateView(CreateView): +class ItemExpenseCreateView(CreateView, LoginRequiredMixin): model = ItemModel form_class = ExpenseItemCreateForm template_name = "items/expenses/expense_create.html" @@ -3484,7 +3513,7 @@ class ItemExpenseCreateView(CreateView): return super().form_valid(form) -class ItemExpenseUpdateView(UpdateView): +class ItemExpenseUpdateView(UpdateView, LoginRequiredMixin): model = ItemModel form_class = ExpenseItemUpdateForm template_name = "items/expenses/expense_update.html" @@ -3503,7 +3532,7 @@ class ItemExpenseUpdateView(UpdateView): return super().form_valid(form) -class ItemExpenseListView(ListView): +class ItemExpenseListView(ListView, LoginRequiredMixin): model = ItemModel template_name = "items/expenses/expenses_list.html" context_object_name = "expenses" @@ -3514,7 +3543,7 @@ class ItemExpenseListView(ListView): return dealer.entity.get_items_expenses() -class BillListView(ListView): +class BillListView(ListView, LoginRequiredMixin): model = BillModel template_name = "ledger/bills/bill_list.html" context_object_name = "bills" @@ -3606,6 +3635,7 @@ class ApprovedBillModelView(LoginRequiredMixin, UpdateView): return super().form_valid(form) +@login_required def bill_mark_as_approved(request, pk): bill = get_object_or_404(BillModel, pk=pk) if request.method == "POST": @@ -3619,6 +3649,7 @@ def bill_mark_as_approved(request, pk): return redirect("bill_detail", pk=bill.pk) +@login_required def bill_mark_as_paid(request, pk): bill = get_object_or_404(BillModel, pk=pk) if request.method == "POST": @@ -3668,6 +3699,7 @@ def bill_mark_as_paid(request, pk): # ) # form.instance.ledger = ledger # return super().form_valid(form) + @login_required def bill_create(request): dealer = get_user_type(request) @@ -3788,21 +3820,20 @@ def bill_create(request): return render(request, "ledger/bills/bill_form.html", context) +@login_required def BillDeleteView(request, pk): bill = get_object_or_404(BillModel, pk=pk) bill.delete() return redirect("bill_list") -class SubscriptionPlans(ListView): +class SubscriptionPlans(ListView, LoginRequiredMixin): model = models.SubscriptionPlan template_name = "subscriptions/subscription_plan.html" context_object_name = "plans" # orders - - class OrderListView(ListView): model = models.SaleOrder template_name = "sales/orders/order_list.html" @@ -3810,6 +3841,7 @@ class OrderListView(ListView): # email +@login_required def send_email_view(request, pk): estimate = get_object_or_404(EstimateModel, pk=pk) if request.method == "POST": @@ -3817,7 +3849,7 @@ def send_email_view(request, pk): # messages.error(request, "Estimate is not ready for review") # return redirect("estimate_detail", pk=estimate.pk) if not estimate.get_itemtxs_data()[0]: - messages.error(request, "Estimate has no items") + messages.error(request, _("Estimate has no items")) return redirect("estimate_detail", pk=estimate.pk) send_email( @@ -3827,7 +3859,7 @@ def send_email_view(request, pk): request.POST.get("message"), ) estimate.mark_as_review() - messages.success(request, "Email sent successfully!") + messages.success(request, _("Email sent successfully!")) return redirect("estimate_detail", pk=estimate.pk) link = reverse_lazy("estimate_preview", kwargs={"pk": estimate.pk}) msg = f""" @@ -3879,34 +3911,7 @@ def custom_bad_request_view(request, exception=None): return render(request, "errors/400.html", {}) -# from django_ledger.io.io_core import get_localdate -# from django_ledger.views.mixins import (DjangoLedgerSecurityMixIn) -# from django.views.generic import RedirectView -from django_ledger.views.financial_statement import ( - FiscalYearBalanceSheetView, - BaseIncomeStatementRedirectView, - FiscalYearIncomeStatementView, - BaseCashFlowStatementRedirectView, - FiscalYearCashFlowStatementView, -) -from django_ledger.views.entity import EntityModelDetailBaseView,EntityModelDetailHandlerView -from django.views.generic import DetailView, RedirectView - -from django_ledger.io.io_core import get_localdate -from django_ledger.models import EntityModel, EntityUnitModel -from django_ledger.views.mixins import ( - QuarterlyReportMixIn, - YearlyReportMixIn, - MonthlyReportMixIn, - DateReportMixIn, - DjangoLedgerSecurityMixIn, - EntityUnitMixIn, - BaseDateNavigationUrlMixIn, - PDFReportMixIn, -) -# BALANCE SHEET ----------- - - +# BALANCE SHEET class BaseBalanceSheetRedirectView(DjangoLedgerSecurityMixIn, RedirectView): def get_redirect_url(self, *args, **kwargs): year = get_localdate().year @@ -3916,23 +3921,23 @@ class BaseBalanceSheetRedirectView(DjangoLedgerSecurityMixIn, RedirectView): ) -class FiscalYearBalanceSheetViewBase(FiscalYearBalanceSheetView): +class FiscalYearBalanceSheetViewBase(FiscalYearBalanceSheetView,DjangoLedgerSecurityMixIn): template_name = "ledger/reports/balance_sheet.html" -class QuarterlyBalanceSheetView(FiscalYearBalanceSheetViewBase, QuarterlyReportMixIn): +class QuarterlyBalanceSheetView(FiscalYearBalanceSheetViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn): """ Quarter Balance Sheet View. """ -class MonthlyBalanceSheetView(FiscalYearBalanceSheetViewBase, MonthlyReportMixIn): +class MonthlyBalanceSheetView(FiscalYearBalanceSheetViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn): """ Monthly Balance Sheet View. """ -class DateBalanceSheetView(FiscalYearBalanceSheetViewBase, DateReportMixIn): +class DateBalanceSheetView(FiscalYearBalanceSheetViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn): """ Date Balance Sheet View. """ @@ -3941,7 +3946,7 @@ class DateBalanceSheetView(FiscalYearBalanceSheetViewBase, DateReportMixIn): # Income Statement ----------- -class BaseIncomeStatementRedirectViewBase(BaseIncomeStatementRedirectView): +class BaseIncomeStatementRedirectViewBase(BaseIncomeStatementRedirectView, DjangoLedgerSecurityMixIn): def get_redirect_url(self, *args, **kwargs): year = get_localdate().year dealer = get_user_type(self.request) @@ -3950,25 +3955,25 @@ class BaseIncomeStatementRedirectViewBase(BaseIncomeStatementRedirectView): ) -class FiscalYearIncomeStatementViewBase(FiscalYearIncomeStatementView): +class FiscalYearIncomeStatementViewBase(FiscalYearIncomeStatementView, DjangoLedgerSecurityMixIn): template_name = "ledger/reports/income_statement.html" class QuarterlyIncomeStatementView( - FiscalYearIncomeStatementViewBase, QuarterlyReportMixIn + FiscalYearIncomeStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn ): """ Quarter Income Statement View. """ -class MonthlyIncomeStatementView(FiscalYearIncomeStatementViewBase, MonthlyReportMixIn): +class MonthlyIncomeStatementView(FiscalYearIncomeStatementViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn): """ Monthly Income Statement View. """ -class DateModelIncomeStatementView(FiscalYearIncomeStatementViewBase, DateReportMixIn): +class DateModelIncomeStatementView(FiscalYearIncomeStatementViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn): """ Date Income Statement View. """ @@ -3977,7 +3982,7 @@ class DateModelIncomeStatementView(FiscalYearIncomeStatementViewBase, DateReport # Cash Flow ----------- -class BaseCashFlowStatementRedirectViewBase(BaseCashFlowStatementRedirectView): +class BaseCashFlowStatementRedirectViewBase(BaseCashFlowStatementRedirectView, DjangoLedgerSecurityMixIn): def get_redirect_url(self, *args, **kwargs): year = get_localdate().year dealer = get_user_type(self.request) @@ -3986,12 +3991,12 @@ class BaseCashFlowStatementRedirectViewBase(BaseCashFlowStatementRedirectView): ) -class FiscalYearCashFlowStatementViewBase(FiscalYearCashFlowStatementView): +class FiscalYearCashFlowStatementViewBase(FiscalYearCashFlowStatementView, DjangoLedgerSecurityMixIn): template_name = "ledger/reports/cash_flow_statement.html" class QuarterlyCashFlowStatementView( - FiscalYearCashFlowStatementViewBase, QuarterlyReportMixIn + FiscalYearCashFlowStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn ): """ Quarter Cash Flow Statement View. @@ -3999,14 +4004,14 @@ class QuarterlyCashFlowStatementView( class MonthlyCashFlowStatementView( - FiscalYearCashFlowStatementViewBase, MonthlyReportMixIn + FiscalYearCashFlowStatementViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn ): """ Monthly Cash Flow Statement View. """ -class DateCashFlowStatementView(FiscalYearCashFlowStatementViewBase, DateReportMixIn): +class DateCashFlowStatementView(FiscalYearCashFlowStatementViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn): """ Date Cash Flow Statement View. """ @@ -4014,7 +4019,7 @@ class DateCashFlowStatementView(FiscalYearCashFlowStatementViewBase, DateReportM # Dashboard -class EntityModelDetailHandlerViewBase(EntityModelDetailHandlerView): +class EntityModelDetailHandlerViewBase(EntityModelDetailHandlerView, DjangoLedgerSecurityMixIn): def get_redirect_url(self, *args, **kwargs): loc_date = get_localdate() @@ -4036,7 +4041,7 @@ class EntityModelDetailHandlerViewBase(EntityModelDetailHandlerView): }) -class EntityModelDetailBaseViewBase(EntityModelDetailBaseView): +class EntityModelDetailBaseViewBase(EntityModelDetailBaseView, DjangoLedgerSecurityMixIn): template_name = "ledger/reports/dashboard.html" def get_context_data(self, **kwargs): @@ -4066,31 +4071,30 @@ class EntityModelDetailBaseViewBase(EntityModelDetailBaseView): return context -class FiscalYearEntityModelDashboardView(EntityModelDetailBaseViewBase): +class FiscalYearEntityModelDashboardView(EntityModelDetailBaseViewBase, DjangoLedgerSecurityMixIn): """ Entity Fiscal Year Dashboard View. """ -class QuarterlyEntityDashboardView(FiscalYearEntityModelDashboardView, QuarterlyReportMixIn): +class QuarterlyEntityDashboardView(FiscalYearEntityModelDashboardView, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn): """ Entity Quarterly Dashboard View. """ -class MonthlyEntityDashboardView(FiscalYearEntityModelDashboardView, MonthlyReportMixIn): +class MonthlyEntityDashboardView(FiscalYearEntityModelDashboardView, MonthlyReportMixIn, DjangoLedgerSecurityMixIn): """ Monthly Entity Dashboard View. """ -class DateEntityDashboardView(FiscalYearEntityModelDashboardView, DateReportMixIn): +class DateEntityDashboardView(FiscalYearEntityModelDashboardView, DateReportMixIn, DjangoLedgerSecurityMixIn): """ Date-specific Entity Dashboard View. """ - class PayableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): http_method_names = ['get'] @@ -4120,6 +4124,7 @@ class PayableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): 'message': 'Unauthorized' }, status=401) + class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): http_method_names = ['get'] @@ -4150,6 +4155,7 @@ class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): 'message': 'Unauthorized' }, status=401) + class PnLAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): http_method_names = ['get'] diff --git a/scripts/dsrpipe.py b/scripts/dsrpipe.py new file mode 100644 index 00000000..0526150c --- /dev/null +++ b/scripts/dsrpipe.py @@ -0,0 +1,140 @@ +""" +title: Deepseek R1 Reasoner and Chat with Realtime Thinking Preview +authors: Ethan Copping +author_url: https://github.com/CoppingEthan +version: 0.3.0 +required_open_webui_version: 0.5.5 +license: MIT +# Acknowledgments +Code used from MCode-Team & Zgccrui +""" + +import json +import httpx +from typing import AsyncGenerator, Callable, Awaitable +from pydantic import BaseModel, Field + + +class Pipe: + class Valves(BaseModel): + DEEPSEEK_API_BASE_URL: str = Field( + default="https://api.deepseek.com/v1", description="Base API endpoint URL" + ) + DEEPSEEK_API_KEY: str = Field( + default="", description="Authentication key for API access" + ) + + def __init__(self): + self.valves = self.Valves() + self.thinking = -1 + self._emitter = None + self.data_prefix = "data: " + + def pipes(self): + try: + headers = {"Authorization": f"Bearer {self.valves.DEEPSEEK_API_KEY}"} + resp = httpx.get( + f"{self.valves.DEEPSEEK_API_BASE_URL}/models", + headers=headers, + timeout=10, + ) + if resp.status_code == 200: + return [ + {"id": m["id"], "name": m["id"]} + for m in resp.json().get("data", []) + ] + except Exception: + pass + return [ + {"id": "deepseek-chat", "name": "deepseek-chat"}, + {"id": "deepseek-reasoner", "name": "deepseek-reasoner"}, + ] + + async def pipe( + self, body: dict, __event_emitter__: Callable[[dict], Awaitable[None]] = None + ) -> AsyncGenerator[str, None]: + self.thinking = -1 + self._emitter = __event_emitter__ + + if not self.valves.DEEPSEEK_API_KEY: + yield json.dumps({"error": "Missing API credentials"}) + return + + req_headers = { + "Authorization": f"Bearer {self.valves.DEEPSEEK_API_KEY}", + "Content-Type": "application/json", + } + + try: + request_data = body.copy() + model_id = request_data["model"].split(".", 1)[-1] + request_data["model"] = model_id + is_reasoner = "reasoner" in model_id.lower() + + messages = request_data["messages"] + for i in reversed(range(1, len(messages))): + if messages[i - 1]["role"] == messages[i]["role"]: + alt_role = ( + "user" if messages[i]["role"] == "assistant" else "assistant" + ) + messages.insert( + i, {"role": alt_role, "content": "[Unfinished thinking]"} + ) + + async with httpx.AsyncClient(http2=True) as client: + async with client.stream( + "POST", + f"{self.valves.DEEPSEEK_API_BASE_URL}/chat/completions", + json=request_data, + headers=req_headers, + timeout=20, + ) as resp: + if resp.status_code != 200: + error_content = (await resp.aread()).decode()[:200] + yield json.dumps( + {"error": f"API error {resp.status_code}: {error_content}"} + ) + return + + async for line in resp.aiter_lines(): + if not line.startswith(self.data_prefix): + continue + + stream_data = json.loads(line[6:]) + choice = stream_data.get("choices", [{}])[0] + + if choice.get("finish_reason"): + return + + delta = choice.get("delta", {}) + + if is_reasoner: + state_marker = self._handle_state(delta) + if state_marker: + yield state_marker + if state_marker == "": + yield "\n" + + content = delta.get("reasoning_content", "") or delta.get( + "content", "" + ) + if content: + yield content + else: + content = delta.get("content", "") + if content: + yield content + + except Exception as e: + yield json.dumps({"error": f"{type(e).__name__}: {str(e)}"}) + + def _handle_state(self, delta: dict) -> str: + if self.thinking == -1 and delta.get("reasoning_content"): + self.thinking = 0 + return "" + + if self.thinking == 0 and not delta.get("reasoning_content"): + self.thinking = 1 + return "\n\n\n" + + return "" diff --git a/templates/crm/opportunities/opportunity_detail.html b/templates/crm/opportunities/opportunity_detail.html index 5a72fc90..259037d7 100644 --- a/templates/crm/opportunities/opportunity_detail.html +++ b/templates/crm/opportunities/opportunity_detail.html @@ -7,16 +7,16 @@

{{ _("Opportunity details")}}

- - + +
-

Others Information

+

{{ _("Other Information")}}

-
Status
Update Status +
{{ _("Status") }}
+ {{ _("Update Status")}}
{{status_form.status}}
-
Stage
Update Stage +
{{ _("Stage") }}
+ {{ _("Update Stage")}}
{{status_form.stage}}
@@ -112,10 +114,10 @@
-

{{ _("Estimated Amount") }}

+

{{ _("Quotation Amount") }}

{% if opportunity.estimate %} - {{ opportunity.estimate.get_cost_estimate }} + {{ opportunity.estimate.get_invoiced_amount.invoice_amount_paid__sum }} {% endif %}

@@ -495,18 +497,7 @@
-
- -
-
- - -
-
- - -
+1
@@ -531,18 +522,6 @@
R
-
- - -
-
- - -
-
- - -
+2
@@ -565,23 +544,7 @@
- - -
-
- - -
-
- - -
-
- - -
-
-
+4
+
@@ -831,7 +794,7 @@
-
+
Ansolo Lazinatov
@@ -850,136 +813,7 @@
- - -
- -
- - -
-
-
Jackson Pollock
-
- Based on emails sent rate, the top 10 users - Mar 27, 2021 - Jackson Pollock - -
6 hours ago
- - -
- - -
- - - - -
- -
- - -
-
-
Ansolo Lazinatov
-
- Based on the percentage of recipients - Jun 24, 2021 - Ansolo Lazinarov - -
Active
- - -
- - -
- - - - -
- -
- - -
-
-
Jackson Pollock
-
- Obtaining leads today - May 19, 2024 - Jackson Pollock - -
6 hours ago
- - -
- - -
- - - - -
- -
- - -
-
-
Ansolo Lazinatov
-
- Sums up the many phases of new and existing businesses. - Aug 19, 2024 - Ansolo Lazinarov - -
Active
- - -
- - -
- - - - -
- -
- - -
-
-
Ansolo Lazinatov
-
- Purchasing-Related Vendors - Aug 19, 2024 - Ansolo Lazinarov - -
Active
- - -
- - -
- - +
@@ -1354,7 +1188,7 @@
-

768kB| Shantinan Mekalan | 21st Dec, 12:56 PM

+

768kB| Shantinan Mekalan | 21st Dec, 12:56 PM

diff --git a/templates/sales/estimates/estimate_detail.html b/templates/sales/estimates/estimate_detail.html index b5a9a3e4..a6ae6a38 100644 --- a/templates/sales/estimates/estimate_detail.html +++ b/templates/sales/estimates/estimate_detail.html @@ -1,28 +1,30 @@ {% extends "base.html" %} {% load i18n %} -{% block title %}{{ _("View Estimate") }}{% endblock title %} +{% block title %}{{ _("View Quotation") }}{% endblock title %} {% block content %}