Compare commits

...

7 Commits

14 changed files with 474 additions and 219 deletions

View File

@ -6,17 +6,64 @@ logger = logging.getLogger(__name__)
def check_create_coa_accounts(task):
logger.info("Checking if all accounts are created")
instance = task.kwargs["dealer"]
entity = instance.entity
coa = entity.get_default_coa()
"""
Hook to verify account creation and handle failures
"""
if task.success:
logger.info("Account creation task completed successfully")
return
for account_data in get_accounts_data():
if entity.get_all_accounts().filter(code=account_data["code"]).exists():
logger.info(f"Default account already exists: {account_data['code']}")
continue
logger.info(f"Default account does not exist: {account_data['code']}")
create_account(entity, coa, account_data)
logger.warning("Account creation task failed, checking status...")
try:
dealer_id = task.kwargs.get('dealer_id')
if not dealer_id:
logger.error("No dealer_id in task kwargs")
return
from .models import Dealer
instance = Dealer.objects.select_related('entity').get(id=dealer_id)
entity = instance.entity
if not entity:
logger.error(f"No entity for dealer {dealer_id}")
return
coa = entity.get_default_coa()
if not coa:
logger.error(f"No COA for entity {entity.id}")
return
# Check which accounts are missing and create them
from .utils import get_accounts_data, create_account
missing_accounts = []
for account_data in get_accounts_data():
if not entity.get_all_accounts().filter(code=account_data["code"]).exists():
missing_accounts.append(account_data)
logger.info(f"Missing account: {account_data['code']}")
if missing_accounts:
logger.info(f"Creating {len(missing_accounts)} missing accounts")
for account_data in missing_accounts:
create_account(entity, coa, account_data)
else:
logger.info("All accounts are already created")
except Exception as e:
logger.error(f"Error in check_create_coa_accounts hook: {e}")
# def check_create_coa_accounts(task):
# logger.info("Checking if all accounts are created")
# instance = task.kwargs["dealer"]
# entity = instance.entity
# coa = entity.get_default_coa()
# for account_data in get_accounts_data():
# if entity.get_all_accounts().filter(code=account_data["code"]).exists():
# logger.info(f"Default account already exists: {account_data['code']}")
# continue
# logger.info(f"Default account does not exist: {account_data['code']}")
# create_account(entity, coa, account_data)
def print_results(task):

View File

@ -685,7 +685,7 @@ class Car(Base):
)
#
additional_services = models.ManyToManyField(
AdditionalServices, related_name="additionals", blank=True, null=True
AdditionalServices, related_name="additionals"
)
cost_price = models.DecimalField(
max_digits=14,
@ -1711,7 +1711,7 @@ class Customer(models.Model):
national_id = models.CharField(
max_length=10, unique=True, verbose_name=_("National ID"), null=True, blank=True
)
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
@ -3307,6 +3307,7 @@ class CustomGroup(models.Model):
"payment",
"vendor",
"additionalservices",
'customer'
],
other_perms=[
"view_car",

View File

@ -135,55 +135,101 @@ def create_car_location(sender, instance, created, **kwargs):
print(f"Failed to create CarLocation for car {instance.vin}: {e}")
# Create Entity
@receiver(post_save, sender=models.Dealer)
def create_ledger_entity(sender, instance, created, **kwargs):
"""
Signal handler for creating ledger entities and initializing accounts for a new Dealer instance upon creation.
This signal is triggered when a new Dealer instance is saved to the database. It performs the following actions:
1. Creates a ledger entity for the Dealer with necessary configurations.
2. Generates a chart of accounts (COA) for the entity and assigns it as the default.
3. Creates predefined unit of measures (UOMs) related to the entity.
4. Initializes and assigns default accounts under various roles (e.g., assets, liabilities) for the entity with their
respective configurations (e.g., account code, balance type).
This function ensures all necessary financial records and accounts are set up when a new Dealer is added, preparing the
system for future financial transactions and accounting operations.
:param sender: The model class that sent the signal (in this case, Dealer).
:param instance: The instance of the model being saved.
:param created: A boolean indicating whether a new record was created.
:param kwargs: Additional keyword arguments passed by the signal.
:return: None
"""
if created:
entity_name = instance.user.dealer.name
entity = EntityModel.create_entity(
name=entity_name,
admin=instance.user,
use_accrual_method=True,
fy_start_month=1,
)
try:
# Use transaction to ensure atomicity
with transaction.atomic():
entity_name = instance.user.dealer.name
entity = EntityModel.create_entity(
name=entity_name,
admin=instance.user,
use_accrual_method=True,
fy_start_month=1,
)
if entity:
instance.entity = entity
instance.save()
coa = entity.create_chart_of_accounts(
assign_as_default=True, commit=True, coa_name=_(f"{entity_name}-COA")
)
if coa:
# Create unit of measures
entity.create_uom(name="Unit", unit_abbr="unit")
for u in models.UnitOfMeasure.choices:
entity.create_uom(name=u[1], unit_abbr=u[0])
if entity:
instance.entity = entity
instance.save(update_fields=['entity'])
# Create COA accounts, background task
# Create COA synchronously first
coa = entity.create_chart_of_accounts(
assign_as_default=True, commit=True,
coa_name=_(f"{entity_name}-COA")
)
if coa:
# Create essential UOMs synchronously
entity.create_uom(name="Unit", unit_abbr="unit")
# Schedule async task after successful synchronous operations
async_task(
func="inventory.tasks.create_coa_accounts",
dealer=instance,
dealer_id=instance.id, # Pass ID instead of object
hook="inventory.hooks.check_create_coa_accounts",
ack_failure=True, # Ensure task failures are acknowledged
sync=False # Explicitly set to async
)
except Exception as e:
logger.error(f"Failed to create ledger entity for dealer {instance.id}: {e}")
# Schedule retry task
async_task(
func="inventory.tasks.retry_entity_creation",
dealer_id=instance.id,
retry_count=0
)
# Create Entity
# @receiver(post_save, sender=models.Dealer)
# def create_ledger_entity(sender, instance, created, **kwargs):
# """
# Signal handler for creating ledger entities and initializing accounts for a new Dealer instance upon creation.
# This signal is triggered when a new Dealer instance is saved to the database. It performs the following actions:
# 1. Creates a ledger entity for the Dealer with necessary configurations.
# 2. Generates a chart of accounts (COA) for the entity and assigns it as the default.
# 3. Creates predefined unit of measures (UOMs) related to the entity.
# 4. Initializes and assigns default accounts under various roles (e.g., assets, liabilities) for the entity with their
# respective configurations (e.g., account code, balance type).
# This function ensures all necessary financial records and accounts are set up when a new Dealer is added, preparing the
# system for future financial transactions and accounting operations.
# :param sender: The model class that sent the signal (in this case, Dealer).
# :param instance: The instance of the model being saved.
# :param created: A boolean indicating whether a new record was created.
# :param kwargs: Additional keyword arguments passed by the signal.
# :return: None
# """
# if created:
# entity_name = instance.user.dealer.name
# entity = EntityModel.create_entity(
# name=entity_name,
# admin=instance.user,
# use_accrual_method=True,
# fy_start_month=1,
# )
# if entity:
# instance.entity = entity
# instance.save()
# coa = entity.create_chart_of_accounts(
# assign_as_default=True, commit=True, coa_name=_(f"{entity_name}-COA")
# )
# if coa:
# # Create unit of measures
# entity.create_uom(name="Unit", unit_abbr="unit")
# for u in models.UnitOfMeasure.choices:
# entity.create_uom(name=u[1], unit_abbr=u[0])
# # Create COA accounts, background task
# async_task(
# func="inventory.tasks.create_coa_accounts",
# dealer=instance,
# hook="inventory.hooks.check_create_coa_accounts",
# )
# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20))
# create_settings(instance.pk)

View File

@ -62,14 +62,117 @@ def create_settings(pk):
)
def create_coa_accounts(**kwargs):
logger.info("creating all accounts are created")
instance = kwargs.get("dealer")
entity = instance.entity
coa = entity.get_default_coa()
def create_coa_accounts(dealer_id, **kwargs):
"""
Create COA accounts with retry logic and proper error handling
"""
from .models import Dealer
from .utils import create_account, get_accounts_data
for account_data in get_accounts_data():
create_account(entity, coa, account_data)
max_retries = 3
retry_delay = 2 # seconds
for attempt in range(max_retries):
try:
logger.info(f"Attempt {attempt + 1} to create accounts for dealer {dealer_id}")
# Get fresh instance from database
instance = Dealer.objects.select_related('entity').get(id=dealer_id)
entity = instance.entity
if not entity:
logger.error(f"No entity found for dealer {dealer_id}")
return False
coa = entity.get_default_coa()
if not coa:
logger.error(f"No COA found for entity {entity.id}")
return False
logger.info("Creating default accounts")
accounts_created = 0
with transaction.atomic():
for account_data in get_accounts_data():
logger.info(f"Creating account: {account_data['code']}")
if create_account(entity, coa, account_data):
accounts_created += 1
logger.info(f"Successfully created {accounts_created} accounts")
return True
except Exception as e:
logger.error(f"Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
logger.info(f"Retrying in {retry_delay} seconds...")
time.sleep(retry_delay * (attempt + 1)) # Exponential backoff
else:
logger.error(f"All {max_retries} attempts failed for dealer {dealer_id}")
# Schedule a cleanup or notification task
async_task(
"inventory.tasks.handle_account_creation_failure",
dealer_id=dealer_id,
error=str(e)
)
return False
def retry_entity_creation(dealer_id, retry_count=0):
"""
Retry entity creation if initial attempt failed
"""
from .models import Dealer
from yourapp.models import EntityModel
max_retries = 3
if retry_count >= max_retries:
logger.error(f"Max retries reached for dealer {dealer_id}")
return
try:
instance = Dealer.objects.get(id=dealer_id)
if not instance.entity:
# Retry entity creation
entity_name = instance.user.dealer.name
entity = EntityModel.create_entity(
name=entity_name,
admin=instance.user,
use_accrual_method=True,
fy_start_month=1,
)
if entity:
instance.entity = entity
instance.save()
logger.info(f"Successfully created entity on retry {retry_count + 1}")
# Now trigger account creation
async_task(
"inventory.tasks.create_coa_accounts",
dealer_id=dealer_id
)
except Exception as e:
logger.error(f"Retry {retry_count + 1} failed: {e}")
# Schedule another retry
async_task(
"inventory.tasks.retry_entity_creation",
dealer_id=dealer_id,
retry_count=retry_count + 1
)
# def create_coa_accounts(**kwargs):
# logger.info("creating all accounts are created")
# instance = kwargs.get("dealer")
# logger.info(f"Dealer Instance : {instance}")
# entity = instance.entity
# coa = entity.get_default_coa()
# logger.info("Creating default accounts")
# for account_data in get_accounts_data():
# logger.info(f"Creating account: {account_data['code']}")
# create_account(entity, coa, account_data)
# def create_coa_accounts1(pk):

View File

@ -494,8 +494,13 @@ def po_item_formset_table(context, po_model, itemtxs_formset, user):
@register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True)
def bill_item_formset_table(context, item_formset):
bill = BillModel.objects.get(uuid=context["view"].kwargs["bill_pk"])
for item in item_formset:
for form in item_formset.forms:
form.fields["item_model"].queryset = form.fields["item_model"].queryset.exclude(
item_role="product"
)
for item in item_formset:
if item:
print(item.fields["item_model"])
item.initial["quantity"] = item.instance.po_quantity
item.initial["unit_cost"] = item.instance.po_unit_cost
# print(item.instance.po_quantity)

View File

@ -15,6 +15,7 @@ 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
@ -2386,7 +2387,19 @@ def get_accounts_data():
def create_account(entity, coa, account_data):
"""
Create account with proper validation and error handling
"""
try:
# Check if account already exists
existing_account = entity.get_all_accounts().filter(
code=account_data["code"]
).first()
if existing_account:
logger.info(f"Account already exists: {account_data['code']}")
return True
account = entity.create_account(
coa_model=coa,
code=account_data["code"],
@ -2395,11 +2408,37 @@ def create_account(entity, coa, account_data):
balance_type=_(account_data["balance_type"]),
active=True,
)
account.role_default = account_data["default"]
account.save()
logger.info(f"Created default account: {account}")
if account:
account.role_default = account_data["default"]
account.save()
logger.info(f"Successfully created account: {account_data['code']}")
return True
except IntegrityError:
logger.warning(f"Account {account_data['code']} already exists (IntegrityError)")
return True
except Exception as e:
logger.error(f"Error creating default account: {account_data['code']}, {e}")
logger.error(f"Error creating account {account_data['code']}: {e}")
return False
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):
@ -2464,9 +2503,9 @@ def force_regenerate_car_image(car):
class CarImageAPIClient:
"""Simple client to handle authenticated requests to the car image API"""
BASE_URL = "http://10.10.1.111:8888"
USERNAME = "faheed"
PASSWORD = "Tenhal@123"
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
@ -2529,7 +2568,7 @@ class CarImageAPIClient:
"make": payload["make"],
"model": payload["model"],
"exterior_color": payload["color"],
"angle": "3/4 rear",
"angle": "front three-quarter",
"reference_image": "",
}

View File

@ -1,132 +1,156 @@
annotated-types
anyio
arrow
asgiref
attrs
Babel
beautifulsoup4
blessed
cattrs
certifi
cffi
charset-normalizer
click
colorama
crispy-bootstrap5
cryptography
cssbeautifier
defusedxml
diff-match-patch
distro
Django
django-allauth
django-appconf
django-appointment
django-background-tasks
django-bootstrap5
django-ckeditor
django-cors-headers
django-countries
django-crispy-forms
django-debug-toolbar
django-easy-audit
django-extensions
django-filter
django-imagekit
django-import-export
django-js-asset
django-ledger
django-manager-utils
django-next-url-mixin
django-ordered-model
django-phonenumber-field
django-picklefield
django-plans
django-q2
django-query-builder
django-schema-graph
django-sequences
django-tables2
django-treebeard
django-widget-tweaks
djangorestframework
djhtml
djlint
docopt
EditorConfig
Faker
fleming
fonttools
fpdf
fpdf2
greenlet
h11
httpcore
httpx
icalendar
idna
jiter
jsbeautifier
json5
jsonpatch
jsonpointer
jwt
langchain
langchain-core
langchain-ollama
langchain-text-splitters
langsmith
luhnchecker
Markdown
markdown-it-py
mdurl
num2words
numpy
ofxtools
ollama
openai
opencv-python
orjson
packaging
pandas
pathspec
phonenumbers
pilkit
pillow
psycopg2-binary
pycparser
pydantic
pydantic_core
Pygments
python-dateutil
python-slugify
python-stdnum
pytz
pyvin
PyYAML
pyzbar
redis
regex
requests
requests-toolbelt
rich
ruff
setuptools
six
sniffio
soupsieve
SQLAlchemy
sqlparse
suds
swapper
tablib
tenacity
text-unidecode
tqdm
types-python-dateutil
typing-inspection
typing_extensions
tzdata
urllib3
wcwidth
zstandard
annotated-types==0.7.0
anyio==4.9.0
arrow==1.3.0
asgiref==3.9.1
attrs==25.3.0
autobahn==24.4.2
Automat==25.4.16
Babel==2.15.0
beautifulsoup4==4.13.4
blessed==1.21.0
cattrs==25.1.1
certifi==2025.7.9
cffi==1.17.1
channels==4.2.2
charset-normalizer==3.4.2
click==8.2.1
colorama==0.4.6
constantly==23.10.4
crispy-bootstrap5==2025.6
cryptography==45.0.5
cssbeautifier==1.15.4
daphne==4.2.1
defusedxml==0.7.1
diff-match-patch==20241021
distro==1.9.0
Django==5.2.4
django-allauth==65.10.0
django-appconf==1.1.0
django-appointment==3.8.0
django-background-tasks==1.2.8
django-bootstrap5==25.1
django-ckeditor==6.7.3
django-cors-headers==4.7.0
django-countries==7.6.1
django-crispy-forms==2.4
django-debug-toolbar==5.2.0
django-easy-audit==1.3.7
django-extensions==4.1
django-filter==25.1
django-imagekit==5.0.0
django-import-export==4.3.8
django-js-asset==3.1.2
django-ledger==0.7.6.1
django-manager-utils==3.1.5
django-next-url-mixin==0.4.0
django-ordered-model==3.7.4
django-phonenumber-field==8.0.0
django-picklefield==3.3
django-plans==2.0.0
django-prometheus==2.4.1
django-q2==1.8.0
django-query-builder==3.2.0
django-schema-graph==3.1.0
django-sequences==3.0
django-tables2==2.7.5
django-treebeard==4.7.1
django-widget-tweaks==1.5.0
djangorestframework==3.16.0
djhtml==3.0.8
djlint==1.36.4
docopt==0.6.2
EditorConfig==0.17.1
Faker==37.4.0
fleming==0.7.0
fonttools==4.58.5
# fpdf==1.7.2
fpdf2==2.8.3
greenlet==3.2.3
gunicorn==23.0.0
h11==0.16.0
h2==4.2.0
hpack==4.1.0
httpcore==1.0.9
httpx==0.28.1
hyperframe==6.1.0
hyperlink==21.0.0
icalendar==6.3.1
idna==3.10
incremental==24.7.2
jiter==0.10.0
jsbeautifier==1.15.4
json5==0.12.0
jsonpatch==1.33
jsonpointer==3.0.0
jwt==1.4.0
langchain==0.3.26
langchain-core==0.3.68
langchain-ollama==0.3.4
langchain-text-splitters==0.3.8
langsmith==0.4.4
luhnchecker==0.0.12
Markdown==3.8.2
markdown-it-py==3.0.0
mdurl==0.1.2
num2words==0.5.14
numpy==2.3.1
ofxtools==0.9.5
ollama==0.5.1
openai==1.93.3
opencv-python==4.11.0.86
orjson==3.10.18
packaging==24.2
pandas==2.3.1
pathspec==0.12.1
phonenumbers==8.13.42
pilkit==3.0
pillow==10.4.0
priority==1.3.0
prometheus_client==0.22.1
psycopg2-binary==2.9.10
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.22
pydantic==2.11.7
pydantic_core==2.33.2
Pygments==2.19.2
pyOpenSSL==25.1.0
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-slugify==8.0.4
python-stdnum==2.1
pytz==2025.2
pyvin==0.0.2
PyYAML==6.0.2
pyzbar==0.1.9
redis==6.2.0
regex==2024.11.6
requests==2.32.4
requests-toolbelt==1.0.0
rich==14.0.0
ruff==0.12.2
service-identity==24.2.0
setuptools==80.9.0
six==1.17.0
sniffio==1.3.1
soupsieve==2.7
SQLAlchemy==2.0.41
sqlparse==0.5.3
suds==1.2.0
swapper==1.3.0
tablib==3.8.0
tenacity==9.1.2
text-unidecode==1.3
tqdm==4.67.1
Twisted==25.5.0
txaio==25.6.1
types-python-dateutil==2.9.0.20250708
typing-inspection==0.4.1
typing_extensions==4.14.1
tzdata==2025.2
urllib3==2.5.0
uvicorn==0.35.0
uvicorn-worker==0.3.0
wcwidth==0.2.13
zope.interface==7.2
zstandard==0.23.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

View File

@ -2,19 +2,9 @@
{% load static %}
{% load django_ledger %}
{% load widget_tweaks %}
{% if bill.get_itemtxs_data.1.total_amount__sum > 0 %}
<form id="bill-update-form"
action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% else %}
<form id="bill-update-form"
hx-trigger="load delay:300ms"
hx-swap="outerHTML"
hx-target="#bill-update-form"
hx-select="#bill-update-form"
hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% endif %}
<div class="container-fluid py-4">
<!-- Page Header -->
<div class="row mb-4">
@ -125,13 +115,13 @@
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-start gap-2">
{% if not item_formset.has_po %}
{% comment %} {% if not item_formset.has_po %}
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
class="btn btn-phoenix-primary">
<i class="fas fa-plus me-1"></i>
{% trans 'New Item' %}
</a>
{% endif %}
{% endif %} {% endcomment %}
<button type="submit" class="btn btn-phoenix-primary">
<i class="fas fa-save me-1"></i>
{% trans 'Save Changes' %}

View File

@ -11,7 +11,7 @@
hx-swap="outerHTML"
hx-select-oob="#toast-container"
hx-indicator="#spinner">
<li class="nav-item">
{% comment %} <p class="navbar-vertical-label text-primary fs-8 text-truncate">{{request.dealer|default:"Apps"}}</p>
<hr class="navbar-vertical-line"> {% endcomment %}
@ -440,7 +440,7 @@
</div>
</a>
</li>
</ul>
</div> {% endcomment %}
{% endif %}
@ -450,10 +450,10 @@
<a class="nav-link ps-2" href="{% if request.is_dealer%}{% url 'ticket_list' request.dealer.slug %} {% else %}#{%endif%}">
<div class="d-flex align-items-center">
{% if user.is_authenticated%}
<span class="nav-link-icon"><span class="fa-solid fa-gear me-1 fs-7"></span></span>
<span class="nav-link-text">{{ request.dealer.user.username }}</span>
{% endif %}
</div>
</a>

View File

@ -50,15 +50,15 @@
{% trans 'Filters' %} <i class="fas fa-sliders-h ms-2"></i>
</h2>
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-3">
<div class="col-md-4">
<label for="start_date" class="form-label">{% trans 'Start Date' %}</label>
<input type="date" class="form-control" id="start_date" name="start_date" value="{{ start_date|default_if_none:'' }}">
</div>
<div class="col-md-3">
<div class="col-md-4">
<label for="end_date" class="form-label">{% trans 'End Date' %}</label>
<input type="date" class="form-control" id="end_date" name="end_date" value="{{ end_date|default_if_none:'' }}">
</div>
<div class="col-md-2">
<div class="col-md-4">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-filter me-2"></i>{% trans 'Filter' %}
</button>