haikal/inventory/utils.py
2025-07-08 17:58:38 +03:00

1456 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import secrets
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 (
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_ledger.models.transactions import TransactionModel
from django_ledger.models.journal_entry import JournalEntryModel
import logging
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.staffmember.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)
models.CarReservation.objects.create(
car=car, reserved_by=request.user, reserved_until=reserved_until
)
car.status = models.CarStatusChoices.RESERVED
car.save()
# --- Logging for Success ---
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"]["selling_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"],
"selling_price": x.item_model.additional_info["car_finance"][
"selling_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"]["selling_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"]["selling_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)
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 entitys 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.additional_info.update({"car_finance": self.car.finances.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.finances.additional_services.clear()
if hasattr(self.car, "custom_cards"):
self.car.custom_cards.delete()
self.car.finances.cost_price = self.transfer.total_price
self.car.finances.selling_price = 0
self.car.finances.discount_amount = 0
self.car.finances.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.finances.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.finances.additional_services.clear()
# if hasattr(car, "custom_cards"):
# car.custom_cards.delete()
# car.finances.cost_price = transfer.total_price
# car.finances.selling_price = 0
# car.finances.discount_amount = 0
# car.finances.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 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):
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(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("selling_price", 0))
print(item.item_model.car.finances)
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"),
"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, "selling_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 = sum(
Decimal(
self._get_nested_value(item, self.CAR_FINANCE_KEY, "discount_amount")
)
for item in self.item_transactions
)
total_price_discounted = total_price - 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(total_discount, 2),
"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)
print(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)
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()
# make_account = entity.get_all_accounts().filter(name=car.id_car_make.name,role=roles.COGS).first()
# if not make_account:
# last_account = entity.get_all_accounts().filter(role=roles.COGS).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}"
# make_account = entity.create_account(
# name=car.id_car_make.name,
# code=code,
# role=roles.COGS,
# coa_model=coa,
# balance_type="debit",
# active=True
# )
# # get or create additional services account
# additional_services_account = entity.get_default_coa_accounts().filter(name="Additional Services",role=roles.COGS).first()
# if not additional_services_account:
# last_account = entity.get_all_accounts().filter(role=roles.COGS).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}"
# additional_services_account = entity.create_account(
# name="Additional Services",
# code=code,
# role=roles.COGS,
# coa_model=coa,
# balance_type="debit",
# active=True
# )
# inventory_account = entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_INVENTORY).first()
# vat_payable_account = entity.get_default_coa_accounts().get(name="VAT Payable", active=True)
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
)
print(e)
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", args=[request.dealer.slug]))
if request.user.is_authenticated:
# email = request.user.email
# first_name = request.user.first_name
# last_name = request.user.last_name
# phone = request.user.phone
# else:
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)
#
order.status = AbstractOrder.STATUS.NEW
order.save()
#
data = response.json()
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
print(data)
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