import json import secrets import logging import datetime import requests from decimal import Decimal from inventory import models from django.urls import reverse from django.conf import settings from django.utils import timezone from django_ledger.io import roles from django.contrib import messages from django.shortcuts import redirect from django_q.tasks import async_task from django.core.mail import send_mail from plans.models import AbstractOrder from django_ledger.models import ( EstimateModel, InvoiceModel, BillModel, VendorModel, ) from django_ledger.models.items import ItemModel from django.utils.translation import get_language from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import gettext_lazy as _ from django.contrib.contenttypes.models import ContentType from django_ledger.models.transactions import TransactionModel from django_ledger.models.journal_entry import JournalEntryModel from django.db import transaction from django_q.models import Schedule as DjangoQSchedule from django_ledger.io import roles logger = logging.getLogger(__name__) def make_random_password( length=10, allowed_chars="abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" ): return "".join(secrets.choice(allowed_chars) for i in range(length)) def get_jwt_token(): """ Fetches a JWT token from an external authentication API. This function sends a POST request to the provided authentication API endpoint with the required headers and payload. It retrieves a JSON Web Token (JWT) if the request is successful or returns None in case of a failure. :raises: This function does not propagate exceptions but catches and logs ``requests.exceptions.RequestException`` if the request fails. :return: A JWT token as a string if the request is successful, or None if an error occurs during the process. :rtype: str or None """ url = "https://carapi.app/api/auth/login" headers = { "accept": "text/plain", "Content-Type": "application/json", } data = { "api_token": settings.CAR_API_TOKEN, "api_secret": settings.CAR_API_SECRET, } logger.debug(f"Attempting to fetch JWT token from: {url}") try: response = requests.post(url, headers=headers, json=data) response.raise_for_status() #logging for success logger.info("Successfully fetched JWT token.") return response.text except requests.exceptions.RequestException as e: #logging for error logger.error( f"HTTP error fetching JWT token from {url}: ", exc_info=True ) print(f"Error obtaining JWT token: {e}") return None def localize_some_words(): """ Localize predefined words and phrases for display purposes. This function is used to localize a set of predefined words or phrases that may be displayed in a user interface or other contexts. These localized words or phrases include terms like "success," "error," and "Forgot Password?". :return: None """ success = _("success") error = _("error") forget = _("Forgot Password?") return None def get_calculations(quotation): """ Calculates and summarizes financial services data related to the cars in a given quotation. It aggregates costs, VAT, and total amounts based on the services available for the cars linked to the quotation. :param quotation: The quotation object containing the cars and their related financial details. :type quotation: Quotation :return: A dictionary with computed financial details including the services data, total service costs, total VAT computed, and the total including VAT. :rtype: dict """ context = {} qc_len = quotation.quotation_cars.count() cars = [x.car for x in quotation.quotation_cars.all()] finances = models.CarFinance.objects.filter(car__in=cars) services = ItemModel.objects.filter(additional_finances__in=finances).all() data = [ { "name": x.name, "price": x.default_amount, "total_price": x.default_amount * qc_len, "vated": float(x.default_amount) * 0.15 * float(qc_len), "total_price_vat": float(x.default_amount) + (float(x.default_amount) * 0.15 * float(qc_len)), } for x in services ] context["services"] = data context["total_cost"] = 0 context["total_vat"] = 0 context["total_cost_vat"] = 0 for k in context["services"]: context["total_cost"] += k["total_price"] context["total_vat"] += k["vated"] context["total_cost_vat"] = float(context["total_cost"]) + float( context["total_vat"] ) return context def send_email(from_, to_, subject, message): """ Send an email with the specified subject and message. This function sends an email from the given sender to the specified recipient with a subject and a message body. It utilizes the provided parameters to construct and send the email. :param from_: str The sender's email address. :param to_: str The recipient's email address. :param subject: str The subject of the email. :param message: str The body/content of the email. :return: None """ subject = subject message = message from_email = from_ recipient_list = [to_] async_task(send_mail,subject, message, from_email, recipient_list) def get_user_type(request): """ Determine the type of user based on the given request object. This function identifies the type of user from the provided request. It checks if the user is a dealer, staff member, or neither based on the attributes of the request object, returning the appropriate associated user type or `None` if the determination cannot be made. :param request: The HTTP request object containing user and role information. :type request: Any :return: A dealer object if the user is a dealer, a dealer object associated with the staff member if the user is a staff member, or None if the user type cannot be identified. :rtype: Optional[Any] """ if request.is_dealer: return request.user.dealer elif request.is_staff: return request.user.staff.dealer return None def get_dealer_from_instance(instance): """ Retrieve the dealer object from the given instance. This function checks whether the given instance's dealer has an attribute `staff`. If the attribute exists, it directly returns the dealer object associated with the instance. :param instance: The instance from which the dealer object is retrieved. :type instance: Any :return: The dealer object associated with the instance. :rtype: Any """ if instance.dealer.staff: return instance.dealer else: return instance.dealer def reserve_car(car, request): """ Reserve a car for a user for 24 hours and update its status to reserved. The function creates a reservation record for the specified car, sets the reservation expiration time, updates the car's status, and notifies the user about the operation outcome through success or error messages. It then redirects to the car detail page. :param car: The car object to be reserved. :param request: The HTTP request object containing the user making the reservation. :return: Redirection to the car's detail page. """ try: # reserved_until = timezone.now() + timezone.timedelta(hours=24) reserved_until = timezone.now() + timezone.timedelta(minutes=1) reservation = models.CarReservation.objects.create( car=car, reserved_by=request.user, reserved_until=reserved_until ) car.status = models.CarStatusChoices.RESERVED car.save() # --- Logging for Success --- DjangoQSchedule.objects.create( name=f"remove_reservation_for_car_with_vin_{car.vin}", func='inventory.tasks.remove_reservation_by_id', args=reservation.pk, schedule_type=DjangoQSchedule.ONCE, next_run=reserved_until, ) logger.info( f"Car {car.pk} ('{car.id_car_make} {car.id_car_model}') reserved successfully " f"by user {request.user}. " f"Reserved until: {reserved_until}." ) messages.success(request, _("Car reserved successfully.")) except Exception as e: # --- Logging for Error --- logger.error( f"Error reserving car {car.pk} ('{car.id_car_make} {car.id_car_model}') " f"for user {request.user} . " f"Error: {e}", exc_info=True ) messages.error(request, f"Error reserving car: {e}") return redirect("car_detail", dealer_slug=request.dealer.slug, slug=car.slug) def calculate_vat_amount(amount): """ Calculates the VAT (Value Added Tax) amount for a given monetary value. The method retrieves the first active VAT rate from the database and applies it to the provided amount. If no active VAT rate is found, the function will return the original amount. :param amount: The monetary value to which the VAT should be applied. :type amount: Decimal :return: A tuple containing the computed VAT amount and the VAT rate as a decimal, or the original amount if no VAT rate is active. :rtype: Tuple[Decimal, Decimal] or Decimal """ vat = models.VatRate.objects.filter(is_active=True).first() if vat: return ((amount * Decimal(vat.rate)).quantize(Decimal("0.01")), vat.rate) return amount def get_car_finance_data(model): """ Fetches car finance data from the provided model instance and calculates various related details including total prices, VAT, discounts, and additional services information. :param model: The model instance that contains car and finance-related transaction data. :type model: Model :return: A dictionary containing detailed information about the car finance transactions, including individual car details, total quantity, calculated totals for prices, VAT, discounts, additional services, and the VAT rate. :rtype: dict """ vat = models.VatRate.objects.filter(is_active=True).first() data = model.get_itemtxs_data()[0].all() total = sum( [ Decimal(item.item_model.additional_info["car_finance"]["marked_price"]) * Decimal(item.ce_quantity or item.quantity) for item in data ] ) additional_services = [] for i in data: if i.item_model.additional_info["additional_services"]: additional_services.extend( [ {"name": x.get("name"), "price": x.get("price")} for x in i.item_model.additional_info["additional_services"] ] ) return { "cars": [ { "vin": x.item_model.additional_info["car_info"]["vin"], "make": x.item_model.additional_info["car_info"]["make"], "model": x.item_model.additional_info["car_info"]["model"], "year": x.item_model.additional_info["car_info"]["year"], "trim": x.item_model.additional_info["car_info"]["mileage"], "cost_price": x.item_model.additional_info["car_finance"]["cost_price"], "marked_price": x.item_model.additional_info["car_finance"][ "marked_price" ], "discount": x.item_model.additional_info["car_finance"][ "discount_amount" ], "quantity": x.ce_quantity or x.quantity, "unit_price": Decimal( x.item_model.additional_info["car_finance"]["total"] ), "total": Decimal(x.item_model.additional_info["car_finance"]["total"]) * Decimal(x.quantity or x.ce_quantity), "total_vat": x.item_model.additional_info["car_finance"]["total_vat"], "additional_services": x.item_model.additional_info[ "additional_services" ], } for x in data ], "quantity": sum((x.quantity or x.ce_quantity) for x in data), "total_price": total, "total_vat": (total * vat.rate) + total, "total_discount": sum( Decimal(x.item_model.additional_info["car_finance"]["discount_amount"]) for x in data ), "grand_total": Decimal(total * vat.rate) + total - Decimal( sum( Decimal(x.item_model.additional_info["car_finance"]["discount_amount"]) for x in data ) ), "additionals": additional_services, "vat": vat.rate, } def get_financial_values(model): """ Calculates financial values for a given model, including VAT, discounts, grand total, and additional services. This function processes financial data related to items in the input model and computes aggregated values like total amounts, VAT amounts, discounts, and more. It also provides detailed information on cars and items as well as any applicable additional services. :param model: The model instance containing financial and item data. It should have the necessary methods and attributes to fetch transactional records and relevant details. :type model: Any :return: A dictionary containing calculated financial details including: - "vat_amount": The computed VAT amount. - "total": The total value of all items before VAT and discounts. - "grand_total": The total after applying discounts and adding VAT. - "discount_amount": The aggregated discount amount. - "vat": The applicable VAT rate. - "car_and_item_info": A list of detailed car and item information. - "additional_services": A list of additional services and their prices. :rtype: dict """ vat = models.VatRate.objects.filter(is_active=True).first() if not model.get_itemtxs_data()[0].exists(): return { "vat_amount": 0, "total": 0, "grand_total": 0, "discount_amount": 0, "vat": 0, "car_and_item_info": [], "additional_services": [], } data = model.get_itemtxs_data()[0].all() for item in data: if not item.item_model.additional_info.get("car_finance"): return { "vat_amount": 0, "total": 0, "grand_total": 0, "discount_amount": 0, "vat": 0, "car_and_item_info": [], "additional_services": [], } if isinstance(model, InvoiceModel): if model.ce_model: data = model.ce_model.get_itemtxs_data()[0].all() else: data = model.get_itemtxs_data()[0].all() total = sum( [ Decimal(item.item_model.additional_info["car_finance"]["marked_price"]) * Decimal(item.ce_quantity or item.quantity) for item in data ] ) discount_amount = sum( Decimal(i.item_model.additional_info["car_finance"]["discount_amount"]) for i in data ) additional_services = [] for i in data: if i.item_model.additional_info["additional_services"]: additional_services.extend( [ {"name": x["name"], "price": x["price"]} for x in i.item_model.additional_info["additional_services"] ] ) grand_total = Decimal(total) - Decimal(discount_amount) vat_amount = round(Decimal(grand_total) * Decimal(vat.rate), 2) car_and_item_info = [ { "info": x.item_model.additional_info["car_info"], "finances": x.item_model.additional_info["car_finance"], "quantity": x.ce_quantity or x.quantity, "total": Decimal( x.item_model.additional_info["car_finance"]["marked_price"] ) * Decimal(x.ce_quantity or x.quantity), } for x in data ] return { "total": total, "discount_amount": discount_amount, "car_and_item_info": car_and_item_info, "additional_services": additional_services, "grand_total": grand_total + vat_amount, "vat_amount": vat_amount, "vat": vat.rate, } # def set_invoice_payment(dealer, entity, invoice, amount, payment_method): # """ # Processes and applies a payment for a specified invoice. This function calculates # finance details, handles associated account transactions, and updates the invoice # status accordingly. # :param dealer: Dealer object responsible for processing the payment # :type dealer: Dealer # :param entity: Entity object associated with the invoice and payment # :type entity: Entity # :param invoice: The invoice object for which the payment is being made # :type invoice: Invoice # :param amount: The amount being paid towards the invoice # :type amount: Decimal # :param payment_method: The payment method used for the transaction # :type payment_method: str # :return: None # """ # calculator = CarFinanceCalculator(invoice) # finance_data = calculator.get_finance_data() # handle_account_process(invoice, amount, finance_data) # if invoice.can_migrate(): # invoice.migrate_state( # user_model=dealer.user, # entity_slug=entity.slug # ) # invoice.make_payment(amount) # invoice.save() def set_bill_payment(dealer, entity, bill, amount, payment_method): """ Sets the payment for a given bill by creating journal entries for the transaction and updating the respective accounts and the bill's status. The function handles the transaction by creating a new journal entry linked with the specified bill, then records debit and credit transactions using the entity’s cash and accounts payable accounts. It finally updates the bill's payment status and persists changes. :param dealer: The dealer making or receiving the payment. :type dealer: Any :param entity: The business entity involved in the payment transaction. :type entity: Any :param bill: The bill object representing the invoice to be paid. :type bill: Any :param amount: The amount to be paid for the bill. :type amount: Decimal :param payment_method: The method used to make the payment (e.g., cash, credit). :type payment_method: Any :return: None """ total_amount = 0 for x in bill.get_itemtxs_data()[0].all(): total_amount += Decimal(x.unit_cost) * Decimal(x.quantity) journal = JournalEntryModel.objects.create( posted=False, description=f"Payment for bill {bill.bill_number}", ledger=bill.ledger, locked=False, origin="Payment", ) cash_account = entity.get_default_coa_accounts().get(name="Cash", active=True) account_payable = entity.get_default_coa_accounts().get( name="Accounts Payable", active=True ) TransactionModel.objects.create( journal_entry=journal, account=cash_account, # Debit Cash amount=amount, # Payment amount tx_type="debit", description="Bill Payment Received", ) TransactionModel.objects.create( journal_entry=journal, account=account_payable, # Credit Accounts Receivable amount=amount, # Payment amount tx_type="credit", description="Bill Payment Received", ) bill.make_payment(amount) bill.save() def transfer_to_dealer(request, cars, to_dealer, remarks=None): """ Transfers a list of cars from one dealer to another, ensuring that all cars originate from the same dealer and logging the transfer. :param request: The HTTP request object which contains information about the current user and context. :type request: HttpRequest :param cars: A list of car objects to transfer. :type cars: list[Car] :param to_dealer: The dealer object representing the destination dealer. :type to_dealer: Dealer :param remarks: Optional remarks to include in the transfer log. Defaults to None. :type remarks: str, optional :return: None :rtype: NoneType :raises ValueError: If no cars are selected for transfer. :raises ValueError: If the cars are not all from the same dealer. :raises ValueError: If the source and destination dealers are the same. """ dealer = get_user_type(request) if not cars: raise ValueError("No cars selected for transfer.") from_dealer = cars[0].dealer # Assume all cars are from the same dealer # Validate that all cars are from the same dealer for car in cars: if car.dealer != from_dealer: raise ValueError("All cars must be from the same dealer.") if from_dealer == to_dealer: raise ValueError("Cannot transfer cars to the same dealer.") # Log the transfer transfer_log = models.CarTransferLog.objects.create( from_dealer=from_dealer, to_dealer=to_dealer, remarks=remarks, ) transfer_log.cars.set(cars) # Associate the cars with the transfer log # Update the dealer for all cars for car in cars: car.dealer = to_dealer car.save() class CarTransfer: """ Handles the process of transferring a car between dealers, automating associated tasks such as creating customers, invoices, products, vendors, and bills, as well as reflecting changes in relevant entities and systems. The purpose of this class is to facilitate a smooth transfer of car ownership from one dealer to another. It ensures that all necessary financial, logistic, and informational processes are executed and updated within the system. The car's status and related entities are adjusted accordingly post-transfer. :ivar car: Car object being transferred. :ivar transfer: Represents the transfer details, including source and destination dealers. :ivar from_dealer: The dealer transferring the car out. :ivar to_dealer: The dealer receiving the car. :ivar customer: Customer entity corresponding to the receiving dealer. :ivar invoice: Invoice entity created for the car transfer. :ivar ledger: Ledger associated with the created invoice. :ivar item: The inventory item corresponding to the car being transferred. :ivar product: The product created in the receiving dealer's ledger. :ivar vendor: Vendor entity related to the transferring dealer. :ivar bill: Bill entity created for the receiving dealer. """ def __init__(self, car, transfer): self.car = car self.transfer = transfer self.from_dealer = transfer.from_dealer self.to_dealer = transfer.to_dealer self.customer = None self.invoice = None self.ledger = None self.item = None self.product = None self.vendor = None self.bill = None def transfer_car(self): self._create_customer() self._create_invoice() self._create_product_in_receiver_ledger() self._create_vendor_and_bill() self._finalize_car_transfer() return True def _create_customer(self): self.customer = self._find_or_create_customer() if self.customer is None: self.customer = self._create_new_customer() def _find_or_create_customer(self): return ( self.from_dealer.entity.get_customers() .filter(email=self.to_dealer.user.email) .first() ) def _create_new_customer(self): customer = self.from_dealer.entity.create_customer( customer_model_kwargs={ "customer_name": self.to_dealer.name, "email": self.to_dealer.user.email, "address_1": self.to_dealer.address, } ) customer.additional_info = {} customer.additional_info.update({"type": "organization"}) customer.save() return customer def _create_invoice(self): self.invoice = self.from_dealer.entity.create_invoice( customer_model=self.customer, terms=InvoiceModel.TERMS_NET_30, cash_account=self.from_dealer.entity.get_default_coa_accounts().get( name="Cash", active=True ), prepaid_account=self.from_dealer.entity.get_default_coa_accounts().get( name="Accounts Receivable", active=True ), coa_model=self.from_dealer.entity.get_default_coa(), ) self.ledger = self.from_dealer.entity.create_ledger(name=str(self.invoice.pk)) self.invoice.ledgar = self.ledger self.ledger.invoicemodel = self.invoice self.ledger.save() self.invoice.save() self._add_car_item_to_invoice() def _add_car_item_to_invoice(self): self.item = ( self.from_dealer.entity.get_items_products() .filter(name=self.car.vin) .first() ) if not self.item: return invoice_itemtxs = { self.item.item_number: { "unit_cost": self.transfer.total_price, "quantity": self.transfer.quantity, "total_amount": self.transfer.total_price, } } invoice_itemtxs = self.invoice.migrate_itemtxs( itemtxs=invoice_itemtxs, commit=True, operation=InvoiceModel.ITEMIZE_APPEND, ) if self.invoice.can_review(): self.invoice.mark_as_review() self.invoice.mark_as_approved( self.from_dealer.entity.slug, self.from_dealer.entity.admin ) self.invoice.save() def _create_product_in_receiver_ledger(self): uom = ( self.to_dealer.entity.get_uom_all().filter(name=self.item.uom.name).first() ) self.product = self.to_dealer.entity.create_item_product( name=self.item.name, uom_model=uom, item_type=self.item.item_type, coa_model=self.to_dealer.entity.get_default_coa(), ) self.product.additional_info = {} self.product.additional_info.update({"car_info": self.car.to_dict()}) self.product.save() def _create_vendor_and_bill(self): self.vendor = self._find_or_create_vendor() self.bill = self.to_dealer.entity.create_bill( vendor_model=self.vendor, terms=BillModel.TERMS_NET_30, cash_account=self.to_dealer.entity.get_default_coa_accounts().get( name="Cash", active=True ), prepaid_account=self.to_dealer.entity.get_default_coa_accounts().get( name="Prepaid Expenses", active=True ), coa_model=self.to_dealer.entity.get_default_coa(), ) self._add_car_item_to_bill() def _find_or_create_vendor(self): vendor = ( self.to_dealer.entity.get_vendors() .filter(vendor_name=self.from_dealer.name) .first() ) if not vendor: vendor = VendorModel.objects.create( entity_model=self.to_dealer.entity, vendor_name=self.from_dealer.name, additional_info={"info": to_dict(self.from_dealer)}, ) return vendor def _add_car_item_to_bill(self): bill_itemtxs = { self.product.item_number: { "unit_cost": self.transfer.total_price, "quantity": self.transfer.quantity, "total_amount": self.transfer.total_price, } } bill_itemtxs = self.bill.migrate_itemtxs( itemtxs=bill_itemtxs, commit=True, operation=BillModel.ITEMIZE_APPEND ) self.bill.additional_info = {} self.bill.additional_info.update({"car_info": self.car.to_dict()}) self.bill.mark_as_review() self.bill.mark_as_approved( self.to_dealer.entity.slug, self.to_dealer.entity.admin ) self.bill.save() def _finalize_car_transfer(self): self.car.dealer = self.to_dealer self.car.vendor = self.vendor self.car.receiving_date = datetime.datetime.now() self.car.additional_services.clear() if hasattr(self.car, "custom_cards"): self.car.custom_cards.delete() self.car.cost_price = self.transfer.total_price self.car.marked_price = 0 self.car.discount_amount = 0 self.car.save() self.car.location.owner = self.to_dealer self.car.location.showroom = self.to_dealer self.car.location.description = "" self.car.location.save() self.car.status = models.CarStatusChoices.AVAILABLE self.transfer.status = models.CarTransferStatusChoices.success self.transfer.active = False self.transfer.save() self.car.save() # def transfer_car(car, transfer): # from_dealer = transfer.from_dealer # to_dealer = transfer.to_dealer # # add transfer.to_dealer as customer in transfer.from_dealer entity # customer = ( # from_dealer.entity.get_customers().filter(email=to_dealer.user.email).first() # ) # if not customer: # customer = from_dealer.entity.create_customer( # customer_model_kwargs={ # "customer_name": to_dealer.name, # "email": to_dealer.user.email, # "address_1": to_dealer.address, # } # ) # customer.additional_info.update({"type": "organization"}) # customer.save() # invoice = from_dealer.entity.create_invoice( # customer_model=customer, # terms=InvoiceModel.TERMS_NET_30, # cash_account=from_dealer.entity.get_default_coa_accounts().get( # name="Cash", active=True # ), # prepaid_account=from_dealer.entity.get_default_coa_accounts().get( # name="Accounts Receivable", active=True # ), # coa_model=from_dealer.entity.get_default_coa(), # ) # ledger = from_dealer.entity.create_ledger(name=str(invoice.pk)) # invoice.ledgar = ledger # ledger.invoicemodel = invoice # ledger.save() # invoice.save() # item = from_dealer.entity.get_items_products().filter(name=car.vin).first() # if not item: # return # invoice_itemtxs = { # item.item_number: { # "unit_cost": transfer.total_price, # "quantity": transfer.quantity, # "total_amount": transfer.total_price, # } # } # invoice_itemtxs = invoice.migrate_itemtxs( # itemtxs=invoice_itemtxs, # commit=True, # operation=InvoiceModel.ITEMIZE_APPEND, # ) # invoice.save() # invoice.mark_as_review() # invoice.mark_as_approved(from_dealer.entity.slug, from_dealer.entity.admin) # # invoice.mark_as_paid(from_dealer.entity.slug, from_dealer.entity.admin) # invoice.save() # # create car item product in to_dealer entity # uom = to_dealer.entity.get_uom_all().filter(name=item.uom.name).first() # # create item product in the reciever ledger # product = to_dealer.entity.create_item_product( # name=item.name, # uom_model=uom, # item_type=item.item_type, # coa_model=to_dealer.entity.get_default_coa(), # ) # product.additional_info.update({"car_info": car.to_dict()}) # product.save() # # add the sender as vendor and create a bill for it # vendor = None # vendor = to_dealer.entity.get_vendors().filter(vendor_name=from_dealer.name).first() # if not vendor: # vendor = VendorModel.objects.create( # entity_model=to_dealer.entity, # vendor_name=from_dealer.name, # additional_info={"info": to_dict(from_dealer)}, # ) # # transfer the car to to_dealer and create items record # bill = to_dealer.entity.create_bill( # vendor_model=vendor, # terms=BillModel.TERMS_NET_30, # cash_account=to_dealer.entity.get_default_coa_accounts().get( # name="Cash", active=True # ), # prepaid_account=to_dealer.entity.get_default_coa_accounts().get( # name="Prepaid Expenses", active=True # ), # coa_model=to_dealer.entity.get_default_coa(), # ) # bill.additional_info = {} # bill_itemtxs = { # product.item_number: { # "unit_cost": transfer.total_price, # "quantity": transfer.quantity, # "total_amount": transfer.total_price, # } # } # bill_itemtxs = bill.migrate_itemtxs( # itemtxs=bill_itemtxs, commit=True, operation=BillModel.ITEMIZE_APPEND # ) # bill.additional_info.update({"car_info": car.to_dict()}) # bill.additional_info.update({"car_finance": car.to_dict()}) # bill.mark_as_review() # bill.mark_as_approved(to_dealer.entity.slug, to_dealer.entity.admin) # bill.save() # car.dealer = to_dealer # car.vendor = vendor # car.receiving_date = datetime.datetime.now() # car.additional_services.clear() # if hasattr(car, "custom_cards"): # car.custom_cards.delete() # car.cost_price = transfer.total_price # car.selling_price = 0 # car.discount_amount = 0 # car.save() # car.location.owner = to_dealer # car.location.showroom = to_dealer # car.location.description = "" # car.location.save() # car.status = models.CarStatusChoices.AVAILABLE # transfer.status = models.CarTransferStatusChoices.success # transfer.active = False # transfer.save() # car.save() # return True def to_dict(obj): """ Convert a Python object's attributes and associated values to a dictionary, with special handling for specific data types such as datetime and object references. :param obj: The Python object to be converted into a dictionary. Object attributes are dynamically introspected to create the dictionary. For objects containing references to other objects, specific attributes (e.g., "pk" or "id") may be used for conversion and representation. :return: A dictionary containing the key-value pairs for the object's attributes. Datetime values are formatted as strings in "YYYY-MM-DD HH:MM:SS" format. All other attributes, including nested object references, are converted to strings. """ obj_dict = vars(obj).copy() if "_state" in vars(obj): del obj_dict["_state"] for key, value in obj_dict.items(): if isinstance(value, datetime.datetime): obj_dict[key] = value.strftime("%Y-%m-%d %H:%M:%S") elif hasattr(value, "pk") or hasattr(value, "id"): try: obj_dict[key] = value.name except AttributeError: obj_dict[key] = str(value) else: obj_dict[key] = str(value) return obj_dict class CarFinanceCalculator1: """ Class responsible for calculating car financing details. This class provides methods and attributes required for calculating various aspects related to car financing, such as VAT calculation, pricing, discounts, and additional services. It processes data about cars, computes totals (e.g., price, VAT, discounts), and aggregates the financial data for reporting or further processing. :ivar model: The data model passed to the calculator for retrieving transaction data. :type model: Any :ivar vat_rate: The current active VAT rate retrieved from the database. :type vat_rate: Decimal :ivar item_transactions: A collection of item transactions retrieved from the model. :type item_transactions: list :ivar additional_services: A list of additional services with details (e.g., name, price, taxable status). :type additional_services: list """ VAT_OBJ_NAME = "vat_rate" CAR_FINANCE_KEY = "car_finance" CAR_INFO_KEY = "car_info" ADDITIONAL_SERVICES_KEY = "additional_services" def __init__(self, model): if isinstance(model, InvoiceModel): self.dealer = models.Dealer.objects.get(entity=model.ce_model.entity) self.extra_info = models.ExtraInfo.objects.get( dealer=self.dealer, content_type=ContentType.objects.get_for_model(model.ce_model), object_id=model.ce_model.pk, ) elif isinstance(model, EstimateModel): self.dealer = models.Dealer.objects.get(entity=model.entity) self.extra_info = models.ExtraInfo.objects.get( dealer=self.dealer, content_type=ContentType.objects.get_for_model(model), object_id=model.pk, ) self.model = model self.vat_rate = self._get_vat_rate() self.item_transactions = self._get_item_transactions() # self.additional_services = self._get_additional_services() def _get_vat_rate(self): vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first() if not vat: raise ObjectDoesNotExist("No active VAT rate found") return vat.rate def _get_additional_services(self): return [x for item in self.item_transactions for x in item.item_model.car.additional_services ] def _get_item_transactions(self): return self.model.get_itemtxs_data()[0].all() def get_items(self): return self._get_item_transactions() @staticmethod def _get_quantity(item): return item.ce_quantity or item.quantity # def _get_nested_value(self, item, *keys): # current = item.item_model.additional_info # for key in keys: # current = current.get(key, {}) # return current def _get_car_data(self, item): quantity = self._get_quantity(item) car = item.item_model.car unit_price = Decimal(car.marked_price) discount = self.extra_info.data.get("discount",0) sell_price = unit_price - Decimal(discount) return { "item_number": item.item_model.item_number, "vin": car.vin, #car_info.get("vin"), "make": car.id_car_make ,#car_info.get("make"), "model": car.id_car_model ,#car_info.get("model"), "year": car.year ,# car_info.get("year"), "logo": car.logo, # getattr(car.id_car_make, "logo", ""), "trim": car.id_car_trim ,# car_info.get("trim"), "mileage": car.mileage ,# car_info.get("mileage"), "cost_price": car.cost_price, "selling_price": car.selling_price, "marked_price": car.marked_price, "discount": car.discount_amount, "quantity": quantity, "unit_price": unit_price, "sell_price": sell_price, "total": unit_price, "total_vat": sell_price * self.vat_rate, "total_discount": discount, "final_price": sell_price + (sell_price * self.vat_rate), "total_additionals": car.total_additional_services, "grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services, "additional_services": car.additional_services,# self._get_nested_value( #item, self.ADDITIONAL_SERVICES_KEY #), } def calculate_totals(self): total_price = sum( Decimal(item.item_model.car.marked_price) for item in self.item_transactions ) total_additionals = sum( Decimal(item.price_) for item in self._get_additional_services()) total_discount = self.extra_info.data.get("discount",0) total_price_discounted = total_price if total_discount: total_price_discounted = total_price - Decimal(total_discount) print(total_price_discounted) total_vat_amount = total_price_discounted * self.vat_rate return { "total_price_discounted":total_price_discounted, "total_price_before_discount":total_price, "total_price": total_price_discounted, "total_vat_amount": total_vat_amount, "total_discount": Decimal(total_discount), "total_additionals": total_additionals, "grand_total":total_price_discounted + total_vat_amount + total_additionals, } def get_finance_data(self): totals = self.calculate_totals() return { "cars": [self._get_car_data(item) for item in self.item_transactions], "quantity": sum( self._get_quantity(item) for item in self.item_transactions ), "total_price": round(totals["total_price"], 2), "total_price_discounted": round(totals["total_price_discounted"], 2), "total_price_before_discount": round(totals["total_price_before_discount"], 2), "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2), "total_vat_amount": round(totals["total_vat_amount"], 2), "total_discount": round(totals["total_discount"], 2), "total_additionals": round(totals["total_additionals"], 2), "grand_total": round(totals["grand_total"], 2), "additionals": self._get_additional_services(), "vat": round(self.vat_rate, 2), } class CarFinanceCalculator: """ Class responsible for calculating car financing details. This class provides methods and attributes required for calculating various aspects related to car financing, such as VAT calculation, pricing, discounts, and additional services. It processes data about cars, computes totals (e.g., price, VAT, discounts), and aggregates the financial data for reporting or further processing. :ivar model: The data model passed to the calculator for retrieving transaction data. :type model: Any :ivar vat_rate: The current active VAT rate retrieved from the database. :type vat_rate: Decimal :ivar item_transactions: A collection of item transactions retrieved from the model. :type item_transactions: list :ivar additional_services: A list of additional services with details (e.g., name, price, taxable status). :type additional_services: list """ VAT_OBJ_NAME = "vat_rate" CAR_FINANCE_KEY = "car_finance" CAR_INFO_KEY = "car_info" ADDITIONAL_SERVICES_KEY = "additional_services" def __init__(self, model): if isinstance(model, InvoiceModel): self.dealer = models.Dealer.objects.get(entity=model.ce_model.entity) self.extra_info = models.ExtraInfo.objects.get( dealer=self.dealer, content_type=ContentType.objects.get_for_model(model.ce_model), object_id=model.ce_model.pk, ) elif isinstance(model, EstimateModel): self.dealer = models.Dealer.objects.get(entity=model.entity) self.extra_info = models.ExtraInfo.objects.get( dealer=self.dealer, content_type=ContentType.objects.get_for_model(model), object_id=model.pk, ) self.model = model self.vat_rate = self._get_vat_rate() self.item_transactions = self._get_item_transactions() # self.additional_services = self._get_additional_services() def _get_vat_rate(self): vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first() if not vat: raise ObjectDoesNotExist("No active VAT rate found") return vat.rate def _get_additional_services(self): return [x for item in self.item_transactions for x in item.item_model.car.additional_services ] def _get_item_transactions(self): return self.model.get_itemtxs_data()[0].all() def get_items(self): return self._get_item_transactions() @staticmethod def _get_quantity(item): return item.ce_quantity or item.quantity # def _get_nested_value(self, item, *keys): # current = item.item_model.additional_info # for key in keys: # current = current.get(key, {}) # return current def _get_car_data(self, item): quantity = self._get_quantity(item) car = item.item_model.car unit_price = Decimal(car.marked_price) discount = self.extra_info.data.get("discount",0) sell_price = unit_price - Decimal(discount) return { "item_number": item.item_model.item_number, "vin": car.vin, #car_info.get("vin"), "make": car.id_car_make ,#car_info.get("make"), "model": car.id_car_model ,#car_info.get("model"), "year": car.year ,# car_info.get("year"), "logo": car.logo, # getattr(car.id_car_make, "logo", ""), "trim": car.id_car_trim ,# car_info.get("trim"), "mileage": car.mileage ,# car_info.get("mileage"), "cost_price": car.cost_price, "selling_price": car.selling_price, "marked_price": car.marked_price, "discount": car.discount_amount, "quantity": quantity, "unit_price": unit_price, "sell_price": sell_price, "total": unit_price, "total_vat": sell_price * self.vat_rate, "total_discount": discount, "final_price": sell_price + (sell_price * self.vat_rate), "total_additionals": car.total_additional_services, "grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services, "additional_services": car.additional_services,# self._get_nested_value( #item, self.ADDITIONAL_SERVICES_KEY #), } def calculate_totals(self): total_price = sum( Decimal(item.item_model.car.marked_price) for item in self.item_transactions ) total_additionals = sum( Decimal(item.price_) for item in self._get_additional_services()) total_discount = self.extra_info.data.get("discount",0) total_price_discounted = total_price if total_discount: total_price_discounted = total_price - Decimal(total_discount) print(total_price_discounted) total_vat_amount = total_price_discounted * self.vat_rate return { "total_price_discounted":total_price_discounted, "total_price_before_discount":total_price, "total_price": total_price_discounted, "total_vat_amount": total_vat_amount, "total_discount": Decimal(total_discount), "total_additionals": total_additionals, "grand_total":total_price_discounted + total_vat_amount + total_additionals, } def get_finance_data(self): totals = self.calculate_totals() return { "car": [self._get_car_data(item) for item in self.item_transactions], "quantity": sum( self._get_quantity(item) for item in self.item_transactions ), "total_price": round(totals["total_price"], 2), "total_price_discounted": round(totals["total_price_discounted"], 2), "total_price_before_discount": round(totals["total_price_before_discount"], 2), "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2), "total_vat_amount": round(totals["total_vat_amount"], 2), "total_discount": round(totals["total_discount"], 2), "total_additionals": round(totals["total_additionals"], 2), "grand_total": round(totals["grand_total"], 2), "additionals": self._get_additional_services(), "vat": round(self.vat_rate, 2), } def get_finance_data(estimate,dealer): vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first() item = estimate.get_itemtxs_data()[0].first() car = item.item_model.car if isinstance(estimate,InvoiceModel) and hasattr(estimate, "ce_model"): estimate = estimate.ce_model extra_info = models.ExtraInfo.objects.get( dealer=dealer, content_type=ContentType.objects.get_for_model(EstimateModel), object_id=estimate.pk, ) discount = extra_info.data.get("discount", 0) discount = Decimal(discount) additional_services = car.get_additional_services() discounted_price=(Decimal(car.marked_price) - discount) vat_amount = discounted_price * vat.rate total_services_vat=sum([x[1] for x in additional_services.get("services")]) total_vat=vat_amount+total_services_vat return { "car": car, "discounted_price": discounted_price or 0, "price_before_discount": car.marked_price, "vat_amount": vat_amount, "vat_rate": vat.rate, "discount_amount": discount, "additional_services": additional_services, "final_price": discounted_price + vat_amount, "total_services_vat":total_services_vat, "total_vat":total_vat, "grand_total": discounted_price + total_vat + additional_services.get("total") } # totals = self.calculate_totals() # return { # "car": [self._get_car_data(item) for item in self.item_transactions], # "quantity": sum( # self._get_quantity(item) for item in self.item_transactions # ), # "total_price": round(totals["total_price"], 2), # "total_price_discounted": round(totals["total_price_discounted"], 2), # "total_price_before_discount": round(totals["total_price_before_discount"], 2), # "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2), # "total_vat_amount": round(totals["total_vat_amount"], 2), # "total_discount": round(totals["total_discount"], 2), # "total_additionals": round(totals["total_additionals"], 2), # "grand_total": round(totals["grand_total"], 2), # "additionals": self._get_additional_services(), # "vat": round(self.vat_rate, 2), # } # class CarFinanceCalculator: # """ # Class responsible for calculating car financing details. # This class provides methods and attributes required for calculating various # aspects related to car financing, such as VAT calculation, pricing, discounts, # and additional services. It processes data about cars, computes totals (e.g., # price, VAT, discounts), and aggregates the financial data for reporting or # further processing. # :ivar model: The data model passed to the calculator for retrieving transaction data. # :type model: Any # :ivar vat_rate: The current active VAT rate retrieved from the database. # :type vat_rate: Decimal # :ivar item_transactions: A collection of item transactions retrieved from the model. # :type item_transactions: list # :ivar additional_services: A list of additional services with details (e.g., name, price, taxable status). # :type additional_services: list # """ # VAT_OBJ_NAME = "vat_rate" # CAR_FINANCE_KEY = "car_finance" # CAR_INFO_KEY = "car_info" # ADDITIONAL_SERVICES_KEY = "additional_services" # def __init__(self, model): # if isinstance(model, InvoiceModel): # self.dealer = models.Dealer.objects.get(entity=model.ce_model.entity) # self.extra_info = models.ExtraInfo.objects.get( # dealer=self.dealer, # content_type=ContentType.objects.get_for_model(model.ce_model), # object_id=model.ce_model.pk, # ) # elif isinstance(model, EstimateModel): # self.dealer = models.Dealer.objects.get(entity=model.entity) # self.extra_info = models.ExtraInfo.objects.get( # dealer=self.dealer, # content_type=ContentType.objects.get_for_model(model), # object_id=model.pk, # ) # self.model = model # self.vat_rate = self._get_vat_rate() # self.item_transactions = self._get_item_transactions() # self.additional_services = self._get_additional_services() # def _get_vat_rate(self): # vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first() # if not vat: # raise ObjectDoesNotExist("No active VAT rate found") # return vat.rate # def _get_item_transactions(self): # return self.model.get_itemtxs_data()[0].all() # @staticmethod # def _get_quantity(item): # return item.ce_quantity or item.quantity # def _get_nested_value(self, item, *keys): # current = item.item_model.additional_info # for key in keys: # current = current.get(key, {}) # return current # def _get_car_data(self, item): # quantity = self._get_quantity(item) # car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY) # car_info = self._get_nested_value(item, self.CAR_INFO_KEY) # unit_price = Decimal(car_finance.get("marked_price", 0)) # return { # "item_number": item.item_model.item_number, # "vin": car_info.get("vin"), # "make": car_info.get("make"), # "model": car_info.get("model"), # "year": car_info.get("year"), # "logo": getattr(item.item_model.car.id_car_make, "logo", ""), # "trim": car_info.get("trim"), # "mileage": car_info.get("mileage"), # "cost_price": car_finance.get("cost_price"), # "selling_price": car_finance.get("selling_price"), # "marked_price": car_finance.get("marked_price"), # "discount": car_finance.get("discount_amount"), # "quantity": quantity, # "unit_price": unit_price, # "total": unit_price * Decimal(quantity), # "total_vat": car_finance.get("total_vat"), # "additional_services": self._get_nested_value( # item, self.ADDITIONAL_SERVICES_KEY # ), # } # def _get_additional_services(self): # return [ # { # "name": service.get("name"), # "price": service.get("price"), # "taxable": service.get("taxable"), # "price_": service.get("price_"), # } # for item in self.item_transactions # for service in self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY) # or [] # ] # def calculate_totals(self): # total_price = sum( # Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, "marked_price")) # * int(self._get_quantity(item)) # for item in self.item_transactions # ) # total_additionals = sum( # Decimal(x.get("price_")) for x in self._get_additional_services() # ) # total_discount = self.extra_info.data.get("discount", 0) # # total_discount = sum( # # Decimal( # # self._get_nested_value(item, self.CAR_FINANCE_KEY, "discount_amount") # # ) # # for item in self.item_transactions # # ) # total_price_discounted = total_price # if total_discount: # total_price_discounted = total_price - Decimal(total_discount) # total_vat_amount = total_price_discounted * self.vat_rate # return { # "total_price_before_discount": round( # total_price, 2 # ), # total_price_before_discount, # "total_price": round(total_price_discounted, 2), # total_price_discounted, # "total_vat_amount": round(total_vat_amount, 2), # total_vat_amount, # "total_discount": round(Decimal(total_discount)), # "total_additionals": round(total_additionals, 2), # total_additionals, # "grand_total": round( # total_price_discounted + total_vat_amount + total_additionals, 2 # ), # } # def get_finance_data(self): # totals = self.calculate_totals() # return { # "cars": [self._get_car_data(item) for item in self.item_transactions], # "quantity": sum( # self._get_quantity(item) for item in self.item_transactions # ), # "total_price": totals["total_price"], # "total_price_before_discount": totals["total_price_before_discount"], # "total_vat": totals["total_vat_amount"] + totals["total_price"], # "total_vat_amount": totals["total_vat_amount"], # "total_discount": totals["total_discount"], # "total_additionals": totals["total_additionals"], # "grand_total": totals["grand_total"], # "additionals": self.additional_services, # "vat": self.vat_rate, # } def get_item_transactions(txs): """ Extracts and compiles relevant transaction details from a list of transactions, including information about cars, financing, estimates, and invoices. The extracted details for each transaction are stored as dictionaries in a list. :param txs: A list of transaction objects from which information is extracted. :type txs: list :return: A list of dictionaries, each containing extracted transaction details. :rtype: list """ transactions = [] for tx in txs: data = {} if tx.item_model.additional_info.get("car_info"): data["info"] = tx.item_model.additional_info.get("car_info") if tx.item_model.additional_info.get("car_finance"): data["finance"] = tx.item_model.additional_info.get("car_finance") if tx.has_estimate(): data["estimate"] = tx.ce_model data["has_estimate"] = True data["customer"] = tx.ce_model.customer if tx.has_invoice(): data["invoice"] = tx.invoice_model data["has_invoice"] = True data["customer"] = tx.invoice_model.customer if bool(data): transactions.append(data) return transactions def get_local_name(self): """ Retrieve the localized name of the instance based on the current language. This method returns the name attribute of an instance based on the currently active language. If the language is Arabic ('ar'), it attempts to return the value of the attribute `arabic_name`. If the language is not Arabic or the `arabic_name` attribute is not defined, it defaults to returning the value of the `name` attribute. :param self: Reference to the instance of the class containing `name` and optionally `arabic_name` attributes. :return: The localized name based on the current language. Returns the value of `arabic_name` for Arabic ('ar') language, or the value of `name` for any other language or if `arabic_name` is not defined. """ if get_language() == "ar": return getattr(self, "arabic_name", None) return getattr(self, "name", None) @transaction.atomic def set_invoice_payment(dealer, entity, invoice, amount, payment_method): """ Records the customer payment (`make_payment`) and posts the full accounting (sales + VAT + COGS + Inventory). """ invoice.make_payment(amount) invoice.save() _post_sale_and_cogs(invoice, dealer) def _post_sale_and_cogs(invoice, dealer): """ For every car line on the invoice: 1) Cash / A-R / VAT / Revenue journal 2) COGS / Inventory journal """ entity = invoice.ledger.entity # calc = CarFinanceCalculator(invoice) data = get_finance_data(invoice,dealer) car = data.get("car") cash_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first() ar_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first() vat_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first() car_rev = entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() add_rev = entity.get_default_coa_accounts().filter(code="4020").first() cogs_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first() inv_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first() # for car_data in data['cars']: # car = invoice.get_itemtxs_data()[0].filter( # item_model__car__vin=car_data['vin'] # ).first().item_model.car # qty = Decimal(car_data['quantity']) net_car_price = Decimal(data['discounted_price']) net_additionals_price = Decimal(data['additional_services']['total']) vat_amount = Decimal(data['vat_amount']) grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount cost_total = Decimal(car.cost_price) discount_amount =Decimal(data['discount_amount']) # ------------------------------------------------------------------ # 2A. Journal: Cash / A-R / VAT / Sales # ------------------------------------------------------------------ je_sale = JournalEntryModel.objects.create( ledger=invoice.ledger, description=f"Sale {car.vin}", origin=f"Invoice {invoice.invoice_number}", locked=False, posted=False ) # Dr Cash (what the customer paid) TransactionModel.objects.create( journal_entry=je_sale, account=cash_acc, amount=grand_total, tx_type='debit', description='Debit to Cash on Hand' ) # # Cr A/R (clear the receivable) # TransactionModel.objects.create( # journal_entry=je_sale, # account=ar_acc, # amount=grand_total, # tx_type='credit' # ) # Cr VAT Payable TransactionModel.objects.create( journal_entry=je_sale, account=vat_acc, amount=vat_amount, tx_type='credit', description="Credit to Tax Payable" ) # Cr Sales – Car TransactionModel.objects.create( journal_entry=je_sale, account=car_rev, amount=net_car_price, tx_type='credit', description=" Credit to Car Sales" ) if car.get_additional_services_amount > 0: # Cr Sales – Additional Services TransactionModel.objects.create( journal_entry=je_sale, account=add_rev, amount=car.get_additional_services_amount, tx_type='credit', description="Credit to After-Sales Services" ) TransactionModel.objects.create( journal_entry=je_sale, account=vat_acc, amount=car.get_additional_services_vat, tx_type='credit', description="Credit to Tax Payable (Additional Services)" ) # ------------------------------------------------------------------ # 2B. Journal: COGS / Inventory reduction # ------------------------------------------------------------------ je_cogs = JournalEntryModel.objects.create( ledger=invoice.ledger, description=f"COGS {car.vin}", origin=f"Invoice {invoice.invoice_number}", locked=False, posted=False ) # Dr COGS TransactionModel.objects.create( journal_entry=je_cogs, account=cogs_acc, amount=cost_total, tx_type='debit', ) # Cr Inventory TransactionModel.objects.create( journal_entry=je_cogs, account=inv_acc, amount=cost_total, tx_type='credit' ) # ------------------------------------------------------------------ # 2C. Update car state flags inside the same transaction # ------------------------------------------------------------------ entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False) # car.item_model.for_inventory = False # car.item_model.save(update_fields=['for_inventory']) car.discount_amount=discount_amount car.selling_price = grand_total # car.is_sold = True car.save() # def handle_account_process(invoice, amount, finance_data): # """ # Processes accounting transactions based on an invoice, financial data, # and related entity accounts configuration. This function handles the # creation of accounts if they do not already exist, and processes journal # entries and transactions. # :param invoice: The invoice object to process transactions for. # :type invoice: InvoiceModel # :param amount: Total monetary value for the transaction. # :type amount: Decimal # :param finance_data: Dictionary containing financial details such as # 'grand_total', 'total_vat_amount', and other related data. # :type finance_data: dict # :return: None # """ # for i in invoice.get_itemtxs_data()[0]: # # car = models.Car.objects.get(vin=invoice.get_itemtxs_data()[0].first().item_model.name) # car = i.item_model.car # entity = invoice.ledger.entity # coa = entity.get_default_coa() # cash_account = ( # entity.get_all_accounts() # .filter(role_default=True, role=roles.ASSET_CA_CASH) # .first() # ) # inventory_account = car.get_inventory_account() # revenue_account = car.get_revenue_account() # cogs_account = car.get_cogs_account() # journal = JournalEntryModel.objects.create( # posted=False, # description=f"Payment for Invoice {invoice.invoice_number}", # ledger=invoice.ledger, # locked=False, # origin=f"Sale of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}", # ) # TransactionModel.objects.create( # journal_entry=journal, # account=cash_account, # amount=Decimal(finance_data.get("grand_total")), # tx_type="debit", # description="", # ) # TransactionModel.objects.create( # journal_entry=journal, # account=revenue_account, # amount=Decimal(finance_data.get("grand_total")), # tx_type="credit", # description="", # ) # journal_cogs = JournalEntryModel.objects.create( # posted=False, # description=f"COGS of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}", # ledger=invoice.ledger, # locked=False, # origin="Payment", # ) # TransactionModel.objects.create( # journal_entry=journal_cogs, # account=cogs_account, # amount=Decimal(car.finances.cost_price), # tx_type="debit", # description="", # ) # TransactionModel.objects.create( # journal_entry=journal_cogs, # account=inventory_account, # amount=Decimal(car.finances.cost_price), # tx_type="credit", # description="", # ) # try: # car.item_model.for_inventory = False # logger.debug(f"Set item_model.for_inventory to False for car {car.vin}.") # except Exception as e: # logger.error( # f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}", # exc_info=True, # ) # car.finances.is_sold = True # car.finances.save() # car.item_model.save() # TransactionModel.objects.create( # journal_entry=journal, # account=additional_services_account, # Debit Additional Services # amount=Decimal(car.finances.total_additionals), # tx_type="debit", # description="Additional Services", # ) # TransactionModel.objects.create( # journal_entry=journal, # account=inventory_account, # Credit Inventory account # amount=Decimal(finance_data.get("grand_total")), # tx_type="credit", # description="Account Adjustment", # ) # TransactionModel.objects.create( # journal_entry=journal, # account=vat_payable_account, # Credit VAT Payable # amount=finance_data.get("total_vat_amount"), # tx_type="credit", # description="VAT Payable on Invoice", # ) def create_make_accounts(dealer): """ Creates accounts for dealer's car makes and associates them with a default Chart of Accounts (CoA) if they don't already exist. The function iterates through all car makes associated with the dealer and creates unique inventory accounts for each car make. Accounts are created with a specified naming convention and are assigned a new account code if required. :param dealer: The dealer for whom to create make accounts. :type dealer: Dealer :return: None :rtype: None """ entity = dealer.entity coa = entity.get_default_coa() # Create a unique account name for the dealer and car make combination makes = models.DealersMake.objects.filter(dealer=dealer).all() for make in makes: account_name = f"{make.car_make.name} Inventory Account" account = ( entity.get_all_accounts().filter(coa_model=coa, name=account_name).first() ) if not account: last_account = ( entity.get_all_accounts() .filter(role=roles.ASSET_CA_INVENTORY) .order_by("-created") .first() ) if len(last_account.code) == 4: code = f"{int(last_account.code)}{1:03d}" elif len(last_account.code) > 4: code = f"{int(last_account.code) + 1}" account = entity.create_account( name=account_name, code=code, role=roles.ASSET_CA_INVENTORY, coa_model=coa, balance_type="credit", active=True, ) def handle_payment(request, order): url = "https://api.moyasar.com/v1/payments" callback_url = request.build_absolute_uri( reverse("payment_callback", kwargs={"dealer_slug": request.dealer.slug}) ) if request.user.is_authenticated: email = request.POST["email"] first_name = request.POST["first_name"] last_name = request.POST["last_name"] phone = request.POST["phone"] card_name = request.POST["card_name"] card_number = str(request.POST["card_number"]).replace(" ", "").strip() month = int(request.POST["card_expiry"].split("/")[0].strip()) year = int(request.POST["card_expiry"].split("/")[1].strip()) cvv = request.POST["card_cvv"] user_data = { "email": email, "first_name": first_name, "last_name": last_name, "phone": phone, } total = int(round(order.total())) * 100 payload = json.dumps( { "amount": total, "currency": "SAR", "description": f"payment issued for {'email'}", "callback_url": callback_url, "source": { "type": "creditcard", "name": card_name, "number": card_number, "month": month, "year": year, "cvc": cvv, "statement_descriptor": "Century Store", "3ds": True, "manual": False, "save_card": False, }, "metadata": user_data, } ) headers = {"Content-Type": "application/json", "Accept": "application/json"} auth = (settings.MOYASAR_SECRET_KEY, "") response = requests.request("POST", url, auth=auth, headers=headers, data=payload) if response.status_code == 400: data = response.json() if data["type"] == "validation_error": errors = data.get("errors", {}) if "source.year" in errors: raise Exception("Invalid expiry year") else: raise Exception("Validation Error: ", errors) else: print("Failed to process payment:", data) # data = response.json() # order.status = AbstractOrder.STATUS.NEW order.save() # amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100))) models.PaymentHistory.objects.create( user=request.user, user_data=user_data, amount=amount, currency=data["currency"], status=data["status"], transaction_id=data["id"], payment_date=data["created_at"], gateway_response=data, ) transaction_url = data["source"]["transaction_url"] return transaction_url # def get_user_quota(user): # return user.dealer.quota def get_accounts_data(): return [ # Current Assets (must start with 1) { "code": "1010", "name": "Cash on Hand", "role": roles.ASSET_CA_CASH, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_CA_CASH }, { "code": "1020", "name": "Bank", "role": roles.ASSET_CA_CASH, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "1030", "name": "Accounts Receivable", "role": roles.ASSET_CA_RECEIVABLES, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_CA_RECEIVABLES }, { "code": "1040", "name": "Inventory (Cars)", "role": roles.ASSET_CA_INVENTORY, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_CA_INVENTORY }, { "code": "1045", "name": "Spare Parts Inventory", "role": roles.ASSET_CA_INVENTORY, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "1050", "name": "Employee Advances", "role": roles.ASSET_CA_RECEIVABLES, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "1060", "name": "Prepaid Expenses", "role": roles.ASSET_CA_PREPAID, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_CA_PREPAID }, { "code": "1070", "name": "Notes Receivable", "role": roles.ASSET_LTI_NOTES_RECEIVABLE, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_LTI_NOTES_RECEIVABLE }, # Fixed Assets (must also start with 1) { "code": "1110", "name": "Lands", "role": roles.ASSET_LTI_LAND, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_LTI_LAND }, { "code": "1111", "name": "Buildings", "role": roles.ASSET_PPE_BUILDINGS, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_PPE_BUILDINGS }, { "code": "1112", "name": "Company Vehicles", "role": roles.ASSET_PPE_EQUIPMENT, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_PPE_EQUIPMENT }, { "code": "1113", "name": "Equipment & Tools", "role": roles.ASSET_PPE_EQUIPMENT, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "1114", "name": "Furniture & Fixtures", "role": roles.ASSET_PPE_EQUIPMENT, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "1115", "name": "Other Fixed Assets", "role": roles.ASSET_PPE_EQUIPMENT, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "1120", "name": "Long-term Investments", "role": roles.ASSET_LTI_SECURITIES, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_LTI_SECURITIES }, { "code": "1130", "name": "Intangible Assets", "role": roles.ASSET_INTANGIBLE_ASSETS, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for ASSET_INTANGIBLE_ASSETS }, # Current Liabilities (must start with 2) { "code": "2010", "name": "Accounts Payable", "role": roles.LIABILITY_CL_ACC_PAYABLE, "balance_type": roles.CREDIT, "locked": True, "default": True, # Default for LIABILITY_CL_ACC_PAYABLE }, { "code": "2020", "name": "Notes Payable", "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, "default": True, # Default for LIABILITY_CL_ST_NOTES_PAYABLE }, { "code": "2030", "name": "Short-term Loans", "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, "default": False, }, { "code": "2040", "name": "Employee Payables", "role": roles.LIABILITY_CL_WAGES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, "default": True, # Default for LIABILITY_CL_WAGES_PAYABLE }, { "code": "2050", "name": "Accrued Expenses", "role": roles.LIABILITY_CL_OTHER, "balance_type": roles.CREDIT, "locked": False, "default": True, # Default for LIABILITY_CL_OTHER }, { "code": "2060", "name": "Accrued Taxes", "role": roles.LIABILITY_CL_TAXES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, "default": False, # Default for LIABILITY_CL_TAXES_PAYABLE }, { "code": "2070", "name": "Provisions", "role": roles.LIABILITY_CL_OTHER, "balance_type": roles.CREDIT, "locked": False, "default": False, }, # Long-term Liabilities (must also start with 2) { "code": "2103", "name": "Deferred Revenue", "role": roles.LIABILITY_CL_DEFERRED_REVENUE, "balance_type": roles.CREDIT, "locked": False, "default": True, # Default for LIABILITY_CL_DEFERRED_REVENUE }, { "code": "2200", "name": "Tax Payable", "role": roles.LIABILITY_CL_TAXES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, "default": True, }, { "code": "2210", "name": "Long-term Bank Loans", "role": roles.LIABILITY_LTL_NOTES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, "default": True, # Default for LIABILITY_LTL_NOTES_PAYABLE }, { "code": "2220", "name": "Lease Liabilities", "role": roles.LIABILITY_LTL_NOTES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, "default": False, }, { "code": "2230", "name": "Other Long-term Liabilities", "role": roles.LIABILITY_LTL_NOTES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, "default": False, }, # Equity (must start with 3) { "code": "3010", "name": "Capital", "role": roles.EQUITY_CAPITAL, "balance_type": roles.CREDIT, "locked": True, "default": True, # Default for EQUITY_CAPITAL }, { "code": "3020", "name": "Statutory Reserve", "role": roles.EQUITY_ADJUSTMENT, "balance_type": roles.CREDIT, "locked": False, "default": True, # Default for EQUITY_ADJUSTMENT }, { "code": "3030", "name": "Retained Earnings", "role": roles.EQUITY_ADJUSTMENT, "balance_type": roles.CREDIT, "locked": False, "default": False, }, { "code": "3040", "name": "Profit & Loss for the Period", "role": roles.EQUITY_ADJUSTMENT, "balance_type": roles.CREDIT, "locked": False, "default": False, }, # Revenue (must start with 4) { "code": "4010", "name": "Car Sales", "role": roles.INCOME_OPERATIONAL, "balance_type": roles.CREDIT, "locked": True, "default": True, # Default for INCOME_OPERATIONAL }, { "code": "4020", "name": "After-Sales Services", "role": roles.INCOME_OPERATIONAL, "balance_type": roles.CREDIT, "locked": False, "default": False, }, { "code": "4030", "name": "Car Rental Income", "role": roles.INCOME_PASSIVE, "balance_type": roles.CREDIT, "locked": False, "default": True, # Default for INCOME_PASSIVE }, { "code": "4040", "name": "Other Income", "role": roles.INCOME_OTHER, "balance_type": roles.CREDIT, "locked": False, "default": True, # Default for INCOME_OTHER }, # Expenses (must start with 5 for COGS, 6 for others) { "code": "5010", "name": "Cost of Goods Sold", "role": roles.COGS, "balance_type": roles.DEBIT, "locked": True, "default": True, # Default for COGS }, { "code": "5015", "name": "Spare Parts Cost Consumed", "role": roles.COGS, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "6010", "name": "Salaries & Wages", "role": roles.EXPENSE_OPERATIONAL, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for EXPENSE_OPERATIONAL }, { "code": "6020", "name": "Rent", "role": roles.EXPENSE_OPERATIONAL, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "6030", "name": "Utilities", "role": roles.EXPENSE_OPERATIONAL, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "6040", "name": "Advertising & Marketing", "role": roles.EXPENSE_OPERATIONAL, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "6050", "name": "Maintenance", "role": roles.EXPENSE_OPERATIONAL, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "6060", "name": "Operating Expenses", "role": roles.EXPENSE_OPERATIONAL, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "6070", "name": "Depreciation", "role": roles.EXPENSE_DEPRECIATION, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for EXPENSE_DEPRECIATION }, { "code": "6080", "name": "Fees & Taxes", "role": roles.EXPENSE_OPERATIONAL, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "6090", "name": "Bank Charges", "role": roles.EXPENSE_OPERATIONAL, "balance_type": roles.DEBIT, "locked": False, "default": False, }, { "code": "6100", "name": "Other Expenses", "role": roles.EXPENSE_OTHER, "balance_type": roles.DEBIT, "locked": False, "default": True, # Default for EXPENSE_OTHER }, ] def create_account(entity, coa, account_data): try: account = entity.create_account( coa_model=coa, code=account_data["code"], name=account_data["name"], role=account_data["role"], balance_type=_(account_data["balance_type"]), active=True, ) account.role_default = account_data["default"] account.save() logger.info(f"Created default account: {account}") except Exception as e: logger.error(f"Error creating default account: {account_data['code']}, {e}")