diff --git a/inventory/hooks.py b/inventory/hooks.py index 36a793b1..ea2b6ae8 100644 --- a/inventory/hooks.py +++ b/inventory/hooks.py @@ -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): diff --git a/inventory/models.py b/inventory/models.py index ee0c2c7a..b8ed7462 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -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, diff --git a/inventory/signals.py b/inventory/signals.py index 0420e045..288ace81 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -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) diff --git a/inventory/tasks.py b/inventory/tasks.py index b33798f4..729fc65d 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -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): diff --git a/inventory/utils.py b/inventory/utils.py index ed94bebf..79b0bd99 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -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): diff --git a/requirements_dev.txt b/requirements_dev.txt index 7aac9770..39e8ffdb 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -63,7 +63,7 @@ EditorConfig==0.17.1 Faker==37.4.0 fleming==0.7.0 fonttools==4.58.5 -fpdf==1.7.2 +# fpdf==1.7.2 fpdf2==2.8.3 greenlet==3.2.3 gunicorn==23.0.0 diff --git a/static/images/car_images/83cbe5eb4176b368963393e278cede416a966f8a4ed59e1b807a591163ba8edb.png b/static/images/car_images/83cbe5eb4176b368963393e278cede416a966f8a4ed59e1b807a591163ba8edb.png new file mode 100644 index 00000000..caf08ce1 Binary files /dev/null and b/static/images/car_images/83cbe5eb4176b368963393e278cede416a966f8a4ed59e1b807a591163ba8edb.png differ diff --git a/static/images/car_images/8bf80ccfd4357469dbcac86c1a473b30c584582639d72d813511b2090d715e21.png b/static/images/car_images/8bf80ccfd4357469dbcac86c1a473b30c584582639d72d813511b2090d715e21.png new file mode 100644 index 00000000..a915a1fb Binary files /dev/null and b/static/images/car_images/8bf80ccfd4357469dbcac86c1a473b30c584582639d72d813511b2090d715e21.png differ diff --git a/static/images/car_images/e48332bac6f6aabb70df995e519667225ae3426f4f400c367da2ebf24d22ae8b.png b/static/images/car_images/e48332bac6f6aabb70df995e519667225ae3426f4f400c367da2ebf24d22ae8b.png new file mode 100644 index 00000000..78dbe4bf Binary files /dev/null and b/static/images/car_images/e48332bac6f6aabb70df995e519667225ae3426f4f400c367da2ebf24d22ae8b.png differ