2886 lines
102 KiB
Python
2886 lines
102 KiB
Python
import os
|
||
import json
|
||
import secrets
|
||
import logging
|
||
import datetime
|
||
import requests
|
||
from PIL import Image
|
||
from io import BytesIO
|
||
from decimal import Decimal
|
||
from inventory import models
|
||
from django.urls import reverse
|
||
from django.conf import settings
|
||
from urllib.parse import urljoin
|
||
from django.utils import timezone
|
||
from django.db import transaction
|
||
from django_ledger.io import roles
|
||
from django.contrib import messages
|
||
from django.db import IntegrityError
|
||
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,
|
||
AccountModel,
|
||
EntityModel,
|
||
ChartOfAccountModel,
|
||
)
|
||
from django.core.files.base import ContentFile
|
||
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_q.models import Schedule as DjangoQSchedule
|
||
from django.contrib.contenttypes.models import ContentType
|
||
from django_ledger.models.transactions import TransactionModel
|
||
from django_ledger.models.journal_entry import JournalEntryModel
|
||
# from .tasks import generate_car_image_task
|
||
|
||
|
||
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_amount = additional_services.get("total")
|
||
total_services_vat = sum([x[1] for x in additional_services.get("services")])
|
||
total_services_amount_ = additional_services.get("total_")
|
||
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_services_amount": total_services_amount,
|
||
"total_services_amount_": total_services_amount_,
|
||
"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: EntityModel = invoice.ledger.entity
|
||
# calc = CarFinanceCalculator(invoice)
|
||
data = get_finance_data(invoice, dealer)
|
||
|
||
car = data.get("car")
|
||
|
||
coa: ChartOfAccountModel = entity.get_default_coa()
|
||
cash_acc = invoice.cash_account or dealer.settings.invoice_cash_account
|
||
|
||
vat_acc = (
|
||
dealer.settings.invoice_tax_payable_account
|
||
or entity.get_default_coa_accounts()
|
||
.filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE)
|
||
.first()
|
||
)
|
||
|
||
car_rev = (
|
||
dealer.settings.invoice_vehicle_sale_account
|
||
or entity.get_default_coa_accounts()
|
||
.filter(role_default=True, role=roles.INCOME_OPERATIONAL)
|
||
.first()
|
||
)
|
||
|
||
add_rev = dealer.settings.invoice_additional_services_account
|
||
|
||
if not add_rev:
|
||
try:
|
||
add_rev = (
|
||
entity.get_default_coa_accounts()
|
||
.filter(name="After-Sales Services", active=True)
|
||
.first()
|
||
)
|
||
if not add_rev:
|
||
add_rev = coa.create_account(
|
||
code="4020",
|
||
name="After-Sales Services",
|
||
role=roles.INCOME_OPERATIONAL,
|
||
balance_type=roles.CREDIT,
|
||
active=True,
|
||
)
|
||
add_rev.role_default = False
|
||
add_rev.save(update_fields=["role_default"])
|
||
dealer.settings.invoice_additional_services_account = add_rev
|
||
dealer.settings.save()
|
||
except Exception as e:
|
||
logger.error(f"error find or create additional services account {e}")
|
||
if car.get_additional_services_amount > 0 and not add_rev:
|
||
raise Exception(
|
||
"additional services exist but not account found,please create account for the additional services and set as default in the settings"
|
||
)
|
||
cogs_acc = (
|
||
dealer.settings.invoice_cost_of_good_sold_account
|
||
or entity.get_default_coa_accounts()
|
||
.filter(role_default=True, role=roles.COGS)
|
||
.first()
|
||
)
|
||
|
||
inv_acc = (
|
||
dealer.settings.invoice_inventory_account
|
||
or entity.get_default_coa_accounts()
|
||
.filter(role_default=True, role=roles.ASSET_CA_INVENTORY)
|
||
.first()
|
||
)
|
||
|
||
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 Sales – Car
|
||
TransactionModel.objects.create(
|
||
journal_entry=je_sale,
|
||
account=car_rev,
|
||
amount=net_car_price,
|
||
tx_type="credit",
|
||
description=" Credit to Car Sales",
|
||
)
|
||
|
||
# Cr VAT Payable
|
||
TransactionModel.objects.create(
|
||
journal_entry=je_sale,
|
||
account=vat_acc,
|
||
amount=vat_amount + car.get_additional_services_vat,
|
||
tx_type="credit",
|
||
description="Credit to Tax Payable",
|
||
)
|
||
|
||
# # Cr A/R (clear the receivable)
|
||
# TransactionModel.objects.create(
|
||
# journal_entry=je_sale,
|
||
# account=ar_acc,
|
||
# amount=grand_total,
|
||
# tx_type='credit'
|
||
# )
|
||
|
||
if car.get_additional_services_amount > 0:
|
||
# Cr Sales – Additional Services
|
||
if not add_rev:
|
||
logger.warning(
|
||
f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry."
|
||
)
|
||
else:
|
||
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, dealer):
|
||
url = "https://api.moyasar.com/v1/payments"
|
||
callback_url = request.build_absolute_uri(
|
||
reverse("payment_callback", kwargs={"dealer_slug": dealer.slug})
|
||
)
|
||
|
||
email = request.POST.get("email")
|
||
first_name = request.POST.get("first_name")
|
||
last_name = request.POST.get("last_name")
|
||
phone = request.POST.get("phone")
|
||
card_name = request.POST.get("card_name")
|
||
card_number = str(request.POST.get("card_number", "")).replace(" ", "").strip()
|
||
expiry = request.POST.get("card_expiry", "").split("/")
|
||
month = int(expiry[0].strip()) if len(expiry) > 0 else 0
|
||
year = int(expiry[1].strip()) if len(expiry) > 1 else 0
|
||
cvv = request.POST.get("card_cvv")
|
||
|
||
user_data = {
|
||
"email": email,
|
||
"first_name": first_name,
|
||
"last_name": last_name,
|
||
"phone": phone,
|
||
}
|
||
|
||
# Get selected plan from session
|
||
selected_plan_id = request.session.get("pending_plan_id")
|
||
if not selected_plan_id:
|
||
raise ValueError("No pending plan found in session")
|
||
from plans.models import PlanPricing
|
||
|
||
pp = PlanPricing.objects.get(pk=selected_plan_id)
|
||
# Calculate amount without creating order
|
||
amount_sar = pp.price # assuming price is in SAR
|
||
tax_amount = amount_sar * 15 / 100
|
||
total = int((amount_sar + tax_amount) * 100) # convert to halalas
|
||
|
||
# Pass plan & dealer info via metadata
|
||
metadata = {
|
||
**user_data,
|
||
"plan_pricing_id": selected_plan_id,
|
||
"dealer_slug": dealer.slug,
|
||
}
|
||
|
||
payload = json.dumps(
|
||
{
|
||
"amount": total,
|
||
"currency": "SAR",
|
||
"description": f"Payment for plan {pp.plan.name}",
|
||
"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": metadata,
|
||
}
|
||
)
|
||
|
||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||
auth = (settings.MOYASAR_SECRET_KEY, "")
|
||
response = requests.post(url, auth=auth, headers=headers, data=payload)
|
||
if response.status_code != 201:
|
||
logger.error(f"Payment initiation failed: {response.text}")
|
||
return None, response.get("message")
|
||
|
||
data = response.json()
|
||
|
||
# Create PaymentHistory WITHOUT linking to order (since order doesn't exist yet)
|
||
amount_decimal = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
|
||
models.PaymentHistory.objects.create(
|
||
user=request.user,
|
||
user_data=json.dumps(metadata),
|
||
amount=amount_decimal,
|
||
currency=data["currency"],
|
||
status=data["status"],
|
||
transaction_id=data["id"],
|
||
payment_date=data["created_at"],
|
||
gateway_response=data,
|
||
)
|
||
logger.info(f"Payment initiated: {data}")
|
||
return data["source"]["transaction_url"], None
|
||
|
||
|
||
# def handle_payment(request, order):
|
||
# logger.info(f"Handling payment for order {order}")
|
||
# url = "https://api.moyasar.com/v1/payments"
|
||
# callback_url = request.build_absolute_uri(
|
||
# reverse("payment_callback", kwargs={"dealer_slug": request.dealer.slug})
|
||
# )
|
||
|
||
# logger.info(f"Got callback_url: {callback_url}")
|
||
|
||
# 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"]
|
||
|
||
# logger.info(f"Got user data: {email}, {first_name}, {last_name}, {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"]
|
||
|
||
# logger.info(f"Got card data: {card_name}, {card_number[:4]}****, {month}, {year}, {cvv}")
|
||
|
||
# user_data = {
|
||
# "email": email,
|
||
# "first_name": first_name,
|
||
# "last_name": last_name,
|
||
# "phone": phone,
|
||
# }
|
||
# try:
|
||
# total = int((order.total() + order.tax * order.total() / 100) * 100)
|
||
# except (AttributeError, TypeError):
|
||
# raise ValueError("Order total or tax is invalid")
|
||
# logger.info(f"Calculated total: {total}")
|
||
|
||
# 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": False,
|
||
# "manual": False,
|
||
# "save_card": False,
|
||
# },
|
||
# "metadata": user_data,
|
||
# }
|
||
# )
|
||
|
||
# logger.info(f"Generated payload: {payload}")
|
||
|
||
# headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||
# auth = (settings.MOYASAR_SECRET_KEY, "")
|
||
# response = requests.request("POST", url, auth=auth, headers=headers, data=payload)
|
||
# logger.info(f"Sent request to Moyasar API: {response.status_code}")
|
||
|
||
# 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:
|
||
# logger.error(f"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"]
|
||
# logger.info(f"Created payment history and got transaction_url: {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):
|
||
logger.info(f"Creating account: {account_data['code']}")
|
||
logger.info(f"COA: {coa}")
|
||
try:
|
||
# Skip if exists
|
||
if coa.get_coa_accounts().filter(code=account_data["code"]).exists():
|
||
logger.info(f"Account already exists: {account_data['code']}")
|
||
return True
|
||
|
||
logger.info(f"Account does not exist: {account_data['code']},creating...")
|
||
account = coa.create_account(
|
||
code=account_data["code"],
|
||
name=account_data["name"],
|
||
role=account_data["role"],
|
||
balance_type=account_data["balance_type"],
|
||
active=True,
|
||
)
|
||
logger.info(f"Created account: {account}")
|
||
if account:
|
||
account.role_default = account_data.get("default", False)
|
||
account.save(update_fields=["role_default"])
|
||
return True
|
||
|
||
except IntegrityError:
|
||
return True # Already created by race condition
|
||
except Exception as e:
|
||
logger.error(f"❌ Error creating {account_data['code']}: {e}")
|
||
|
||
return False
|
||
|
||
|
||
# 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,
|
||
# )
|
||
# logger.info(f"Created account: {account}")
|
||
# 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}")
|
||
|
||
|
||
def get_or_generate_car_image(car):
|
||
"""
|
||
Utility function to get or generate car image asynchronously
|
||
"""
|
||
try:
|
||
car_image, created = models.CarImage.objects.get_or_create(car=car)
|
||
|
||
if created:
|
||
car_image.image_hash = car.get_hash
|
||
car_image.save()
|
||
|
||
if car_image.image:
|
||
return car_image.image.url
|
||
|
||
# Check for existing image with same hash
|
||
existing = (
|
||
models.CarImage.objects.filter(
|
||
image_hash=car_image.image_hash, image__isnull=False
|
||
)
|
||
.exclude(car=car)
|
||
.first()
|
||
)
|
||
|
||
if existing:
|
||
car_image.image.save(existing.image.name, existing.image.file, save=True)
|
||
return car_image.image.url
|
||
|
||
# If no image exists and not already generating, schedule generation
|
||
if not car_image.is_generating:
|
||
car_image.schedule_image_generation()
|
||
|
||
return None # Return None while image is being generated
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting/generating car image: {e}")
|
||
return None
|
||
|
||
|
||
def force_regenerate_car_image(car):
|
||
"""
|
||
Force regeneration of car image (useful for admin actions)
|
||
"""
|
||
try:
|
||
car_image, created = models.CarImage.objects.get_or_create(car=car)
|
||
car_image.image_hash = car.get_hash
|
||
car_image.image.delete(save=False) # Remove old image
|
||
car_image.is_generating = False
|
||
car_image.generation_attempts = 0
|
||
car_image.last_generation_error = None
|
||
car_image.save()
|
||
|
||
car_image.schedule_image_generation()
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error forcing image regeneration: {e}")
|
||
return False
|
||
|
||
|
||
class CarImageAPIClient:
|
||
"""Simple client to handle authenticated requests to the car image API"""
|
||
|
||
BASE_URL = settings.TENHAL_IMAGE_GENERATOR_URL
|
||
USERNAME = settings.TENHAL_IMAGE_GENERATOR_USERNAME
|
||
PASSWORD = settings.TENHAL_IMAGE_GENERATOR_PASSWORD
|
||
|
||
def __init__(self):
|
||
self.session = None
|
||
self.csrf_token = None
|
||
|
||
def login(self):
|
||
"""Login to the API and maintain session"""
|
||
try:
|
||
# Start fresh session
|
||
self.session = requests.Session()
|
||
|
||
# Step 1: Get CSRF token
|
||
response = self.session.get(f"{self.BASE_URL}/login")
|
||
response.raise_for_status()
|
||
|
||
# Get CSRF token from cookies
|
||
self.csrf_token = self.session.cookies.get("csrftoken")
|
||
if not self.csrf_token:
|
||
raise Exception("CSRF token not found in cookies")
|
||
|
||
# Step 2: Login
|
||
login_data = {
|
||
"username": self.USERNAME,
|
||
"password": self.PASSWORD,
|
||
"csrfmiddlewaretoken": self.csrf_token,
|
||
}
|
||
|
||
login_response = self.session.post(
|
||
f"{self.BASE_URL}/login", data=login_data
|
||
)
|
||
|
||
if login_response.status_code != 200:
|
||
raise Exception(
|
||
f"Login failed with status {login_response.status_code}"
|
||
)
|
||
|
||
logger.info("Successfully logged in to car image API")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Login failed: {e}")
|
||
self.session = None
|
||
self.csrf_token = None
|
||
return False
|
||
|
||
def generate_image(self, payload):
|
||
"""Generate car image using authenticated session"""
|
||
if not self.session or not self.csrf_token:
|
||
if not self.login():
|
||
raise Exception("Cannot generate image: Login failed")
|
||
|
||
try:
|
||
headers = {
|
||
"X-CSRFToken": self.csrf_token,
|
||
"Referer": self.BASE_URL,
|
||
}
|
||
logger.info(f"Generating image with payload: {payload}")
|
||
generate_data = {
|
||
"year": payload["year"],
|
||
"make": payload["make"],
|
||
"model": payload["model"],
|
||
"exterior_color": payload["color"],
|
||
"angle": "front three-quarter",
|
||
"reference_image": "",
|
||
}
|
||
|
||
response = self.session.post(
|
||
f"{self.BASE_URL}/generate",
|
||
json=generate_data,
|
||
headers=headers,
|
||
timeout=160,
|
||
)
|
||
|
||
response.raise_for_status()
|
||
|
||
# Parse response
|
||
result = response.json()
|
||
image_url = result.get("url")
|
||
logger.info(f"Generated image URL: {image_url}")
|
||
|
||
if not image_url:
|
||
raise Exception("No image URL in response")
|
||
|
||
# Download the actual image
|
||
image_response = self.session.get(
|
||
f"{self.BASE_URL}{image_url}", timeout=160
|
||
)
|
||
|
||
image_response.raise_for_status()
|
||
|
||
return image_response.content, None
|
||
|
||
except requests.RequestException as e:
|
||
error_msg = f"API request failed: {e}"
|
||
logger.error(error_msg)
|
||
return None, error_msg
|
||
except Exception as e:
|
||
error_msg = f"Unexpected error: {e}"
|
||
logger.error(error_msg)
|
||
return None, error_msg
|
||
|
||
|
||
# Global client instance
|
||
api_client = CarImageAPIClient()
|
||
|
||
|
||
def resize_image(image_data, max_size=(800, 600)):
|
||
"""
|
||
Resize image to make it smaller while maintaining aspect ratio
|
||
Returns resized image data in the same format
|
||
"""
|
||
try:
|
||
img = Image.open(BytesIO(image_data))
|
||
original_format = img.format
|
||
original_mode = img.mode
|
||
|
||
# Calculate new size maintaining aspect ratio
|
||
img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||
|
||
# Save back to bytes in original format
|
||
output_buffer = BytesIO()
|
||
|
||
if original_format and original_format.upper() in ["JPEG", "JPG"]:
|
||
img.save(output_buffer, format="JPEG", quality=95, optimize=True)
|
||
elif original_format and original_format.upper() == "PNG":
|
||
# Preserve transparency for PNG
|
||
if original_mode == "RGBA":
|
||
img.save(output_buffer, format="PNG", optimize=True)
|
||
else:
|
||
img.save(output_buffer, format="PNG", optimize=True)
|
||
else:
|
||
# Default to JPEG for other formats
|
||
if img.mode in ("RGBA", "LA", "P"):
|
||
# Convert to RGB if image has transparency
|
||
background = Image.new("RGB", img.size, (255, 255, 255))
|
||
if img.mode == "RGBA":
|
||
background.paste(img, mask=img.split()[3])
|
||
else:
|
||
background.paste(img, (0, 0))
|
||
img = background
|
||
img.save(output_buffer, format="JPEG", quality=95, optimize=True)
|
||
|
||
resized_data = output_buffer.getvalue()
|
||
|
||
logger.info(
|
||
f"Resized image from {len(image_data)} to {len(resized_data)} bytes"
|
||
)
|
||
return resized_data, None
|
||
|
||
except Exception as e:
|
||
error_msg = f"Image resizing failed: {e}"
|
||
logger.error(error_msg)
|
||
return None, error_msg
|
||
|
||
|
||
def generate_car_image_simple(car):
|
||
"""
|
||
Simple function to generate car image with authentication and resizing
|
||
"""
|
||
|
||
# Prepare payload
|
||
payload = {
|
||
"make": car.id_car_make.name if car.id_car_make else "",
|
||
"model": car.id_car_model.name if car.id_car_model else "",
|
||
"year": car.year,
|
||
"color": car.colors.exterior.name,
|
||
}
|
||
|
||
logger.info(f"Generating image for car {car.vin}")
|
||
|
||
# Generate image using API client
|
||
image_data, error = api_client.generate_image(payload)
|
||
|
||
if error:
|
||
return {"success": False, "error": error}
|
||
|
||
if not image_data:
|
||
return {"success": False, "error": "No image data received"}
|
||
|
||
try:
|
||
# Resize the image to make it smaller
|
||
resized_data, resize_error = resize_image(image_data, max_size=(800, 600))
|
||
|
||
if resize_error:
|
||
# If resizing fails, use original image but log warning
|
||
logger.warning(f"Resizing failed, using original image: {resize_error}")
|
||
resized_data = image_data
|
||
|
||
# Determine file extension based on content
|
||
try:
|
||
img = Image.open(BytesIO(resized_data))
|
||
file_extension = img.format.lower() if img.format else "jpg"
|
||
except:
|
||
file_extension = "jpg"
|
||
|
||
# Save the resized image
|
||
logger.info(f" {car.vin}")
|
||
with open(
|
||
os.path.join(
|
||
settings.MEDIA_ROOT, f"car_images/{car.get_hash}.{file_extension}"
|
||
),
|
||
"wb",
|
||
) as f:
|
||
f.write(resized_data)
|
||
logger.info(f"Saved image for car {car.vin}")
|
||
|
||
# Update CarImage record
|
||
logger.info(f"Successfully generated and resized image for car {car.vin}")
|
||
return {"success": True}
|
||
|
||
except Exception as e:
|
||
error_msg = f"Image processing failed: {e}"
|
||
logger.error(error_msg)
|
||
return {"success": False, "error": error_msg}
|
||
|
||
|
||
def create_estimate_(dealer, car, customer):
|
||
entity = dealer.entity
|
||
title = f"Estimate for {car.vin}-{car.id_car_make.name}-{car.id_car_model.name}-{car.year} for customer {customer.first_name} {customer.last_name}"
|
||
estimate = entity.create_estimate(
|
||
estimate_title=title,
|
||
customer_model=customer.customer_model,
|
||
contract_terms="fixed",
|
||
)
|
||
|
||
estimate_itemtxs = {
|
||
car.item_model.item_number: {
|
||
"unit_cost": round(float(car.marked_price)),
|
||
"unit_revenue": round(float(car.marked_price)),
|
||
"quantity": 1,
|
||
"total_amount": round(float(car.final_price_plus_vat)),
|
||
}
|
||
}
|
||
|
||
try:
|
||
estimate.migrate_itemtxs(
|
||
itemtxs=estimate_itemtxs,
|
||
commit=True,
|
||
operation=EstimateModel.ITEMIZE_APPEND,
|
||
)
|
||
except Exception as e:
|
||
estimate.delete()
|
||
raise e
|
||
|
||
return estimate
|