From f2cd3f6969c59a9eac9bf6e281afa6096a61859b Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 10 Sep 2025 14:49:18 +0300 Subject: [PATCH] fix the coa issue --- inventory/hooks.py | 15 ++++--- inventory/models.py | 4 -- inventory/signals.py | 97 +++++++++++++++++++++------------------- inventory/tasks.py | 104 ++++++++++++++++++++----------------------- inventory/utils.py | 37 ++++++--------- 5 files changed, 122 insertions(+), 135 deletions(-) diff --git a/inventory/hooks.py b/inventory/hooks.py index 5fca2a32..b972523c 100644 --- a/inventory/hooks.py +++ b/inventory/hooks.py @@ -9,6 +9,8 @@ def check_create_coa_accounts(task): """ Hook to verify account creation and handle failures """ + from .models import Dealer + if task.success: logger.info("Account creation task completed successfully") return @@ -16,14 +18,15 @@ def check_create_coa_accounts(task): logger.warning("Account creation task failed, checking status...") try: - dealer_id = task.kwargs.get('dealer_id') + dealer_id = task.kwargs.get('dealer_id',None) coa_slug = task.kwargs.get('coa_slug', None) + logger.info(f"Checking accounts for dealer {dealer_id}") + logger.info(f"COA slug: {coa_slug}") 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) + instance = Dealer.objects.get(id=dealer_id) entity = instance.entity if not entity: @@ -34,11 +37,11 @@ def check_create_coa_accounts(task): try: coa = entity.get_coa_model_qs().get(slug=coa_slug) except Exception as e: - logger.error(f"COA with slug {coa_slug} not found for entity {entity.id}: {e}") + logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") else: coa = entity.get_default_coa() if not coa: - logger.error(f"No COA for entity {entity.id}") + logger.error(f"No COA for entity {entity.pk}") return # Check which accounts are missing and create them @@ -46,7 +49,7 @@ def check_create_coa_accounts(task): missing_accounts = [] for account_data in get_accounts_data(): - if not entity.get_all_accounts().filter(code=account_data["code"]).exists(): + if not entity.get_all_accounts().filter(coa_model=coa,code=account_data["code"]).exists(): missing_accounts.append(account_data) logger.info(f"Missing account: {account_data['code']}") diff --git a/inventory/models.py b/inventory/models.py index 33e351f5..66f6e653 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -658,16 +658,12 @@ class Car(Base): CarMake, models.DO_NOTHING, db_column="id_car_make", - null=True, - blank=True, verbose_name=_("Make"), ) id_car_model = models.ForeignKey( CarModel, models.DO_NOTHING, db_column="id_car_model", - null=True, - blank=True, verbose_name=_("Model"), ) year = models.IntegerField(verbose_name=_("Year")) diff --git a/inventory/signals.py b/inventory/signals.py index 083e6487..4dbea6dc 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -138,50 +138,36 @@ def create_car_location(sender, instance, created, **kwargs): @receiver(post_save, sender=models.Dealer) def create_ledger_entity(sender, instance, created, **kwargs): - if created: - 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 not created: + return - if entity: - instance.entity = entity - instance.save(update_fields=['entity']) + try: + with transaction.atomic(): + # Create entity + entity = models.EntityModel.create_entity( + name=instance.user.dealer.name, + admin=instance.user, + use_accrual_method=True, + fy_start_month=1, + ) + if not entity: + raise Exception("Entity creation failed") - # Create COA synchronously first - coa = entity.create_chart_of_accounts( - assign_as_default=True, commit=True, - coa_name=_(f"{entity_name}-COA") - ) + instance.entity = entity + instance.save(update_fields=['entity']) - if coa: - # Create essential UOMs synchronously - for u in models.UnitOfMeasure.choices: - entity.create_uom(name=u[1], unit_abbr=u[0]) - - # Schedule async task after successful synchronous operations - async_task( - func="inventory.tasks.create_coa_accounts", - 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 + # Create default COA + entity.create_chart_of_accounts( + assign_as_default=True, + commit=True, + coa_name=f"{entity.name}-COA" ) - 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 - ) + logger.info(f"✅ Setup complete for dealer {instance.id}: entity & COA ready.") + + except Exception as e: + logger.error(f"💥 Failed setup for dealer {instance.id}: {e}") + # Optional: schedule retry or alert # Create Entity # @receiver(post_save, sender=models.Dealer) # def create_ledger_entity(sender, instance, created, **kwargs): @@ -1406,6 +1392,7 @@ def handle_user_registration(sender, instance, created, **kwargs): if instance.is_created: logger.info(f"User account created: {instance.email}, sending email") + # instance.create_account() send_email( settings.DEFAULT_FROM_EMAIL, instance.email, @@ -1430,15 +1417,31 @@ def handle_user_registration(sender, instance, created, **kwargs): @receiver(post_save, sender=ChartOfAccountModel) def handle_chart_of_account(sender, instance, created, **kwargs): if created: + entity = instance.entity + dealer = instance.entity.admin.dealer + # Create UOMs (minimal, no logging per item) + if not entity.get_uom_all(): + for code, name in models.UnitOfMeasure.choices: + entity.create_uom(name=name, unit_abbr=code) + try: - dealer = instance.entity.dealers.first() - async_task( - func="inventory.tasks.create_coa_accounts", - dealer_id=dealer.pk, # Pass ID instead of object - coa_slug=instance.slug, - hook="inventory.hooks.check_create_coa_accounts", - ack_failure=True, # Ensure task failures are acknowledged - sync=False # Explicitly set to async + # Schedule async account creation AFTER commit + transaction.on_commit( + lambda: async_task( + "inventory.tasks.create_coa_accounts", + dealer_id=dealer.pk, + coa_slug=instance.slug, + hook="inventory.hooks.check_create_coa_accounts", + ack_failure=True, + ) ) + # async_task( + # func="inventory.tasks.create_coa_accounts", + # dealer_id=dealer.pk, # Pass ID instead of object + # coa_slug=instance.slug, + # 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"Error handling chart of account: {e}") \ No newline at end of file diff --git a/inventory/tasks.py b/inventory/tasks.py index f121e0bd..2faaad04 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -18,7 +18,7 @@ from django.core.files.base import ContentFile from django.contrib.auth import get_user_model from allauth.account.models import EmailAddress from django.core.mail import EmailMultiAlternatives -from .utils import get_accounts_data, create_account +# from .utils import get_accounts_data, create_account from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User, Group, Permission @@ -63,70 +63,64 @@ def create_settings(pk): ) -def create_coa_accounts(dealer_id, **kwargs): +def create_coa_accounts(dealer_id,**kwargs): """ - Create COA accounts with retry logic and proper error handling + Idempotent: Creates only missing default accounts. + Safe to retry. Returns True if all done. """ from .models import Dealer - from .utils import create_account, get_accounts_data + from .utils import get_accounts_data, create_account + try: + dealer = Dealer.objects.get(pk=dealer_id) + entity = dealer.entity + coa_slug = kwargs.get('coa_slug', None) + if not entity: + logger.error(f"❌ No entity for dealer {dealer_id}") + return False - max_retries = 3 - retry_delay = 2 # seconds - coa_slug = kwargs.get('coa_slug', None) - logger.info(f"chart of account model slug {coa_slug}") - logger.info(f"Attempting to create accounts for dealer {dealer_id}") - for attempt in range(max_retries): - try: - logger.info(f"Attempt {attempt + 1} to create accounts for dealer {dealer_id}") - - instance = Dealer.objects.get(pk=dealer_id) - entity = instance.entity - - if not entity: - logger.error(f"No entity found for dealer {dealer_id}") + if coa_slug: + try: + coa = entity.get_coa_model_qs().get(slug=coa_slug) + except Exception as e: + logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") return False + else: + coa = entity.get_default_coa() - if coa_slug: - try: - coa = entity.get_coa_model_qs().get(slug=coa_slug) - logger.info(f"COA with slug {coa_slug} found for entity {entity.pk}") - except Exception as e: - logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") - else: - coa = entity.get_default_coa() - logger.info(f"Default COA found for entity {entity.pk}") + if not coa: + logger.error(f"❌ No default COA for entity {entity.pk}") + return False - if not coa: - logger.error(f"No COA found for entity {entity.pk}") - return False + # Get missing accounts + existing_codes = set(entity.get_all_accounts().filter(coa_model=coa).values_list('code', flat=True)) + accounts_to_create = [ + acc for acc in get_accounts_data() + if acc["code"] not in existing_codes + ] - 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") + if not accounts_to_create: + logger.info("✅ All default accounts already exist.") return True - except Exception as e: - logger.error(f"Attempt {attempt + 1} failed: {e}") + # Create missing ones + logger.info(f"🔧 Creating {len(accounts_to_create)} missing accounts...") + success = True - 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 + for acc in accounts_to_create: + if not create_account(entity, coa, acc): + logger.warning(f"⚠️ Failed to create account: {acc['code']}") + success = False # don't fail task, just log + + if success: + logger.info("✅ All missing accounts created successfully.") + else: + logger.warning("⚠️ Some accounts failed to create — check logs.") + + return success # Django-Q will mark as failed if False + + except Exception as e: + logger.error(f"💥 Task failed for dealer {dealer_id}: {e}") + raise # Let Django-Q handle retry if configured def retry_entity_creation(dealer_id, retry_count=0): """ @@ -161,7 +155,7 @@ def retry_entity_creation(dealer_id, retry_count=0): # Now trigger account creation async_task( "inventory.tasks.create_coa_accounts", - dealer_id=dealer_id + dealer_id=dealer_id, ) except Exception as e: diff --git a/inventory/utils.py b/inventory/utils.py index 0305bb72..eb4cc5f4 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -2388,45 +2388,36 @@ def get_accounts_data(): def create_account(entity, coa, account_data): - """ - Create account with proper validation and error handling - """ + logger.info(f"Creating account: {account_data['code']}") + logger.info(f"COA: {coa}") try: - # existing_account = AccountModel.objects.filter(coa_model=coa,code=account_data["code"]) - existing_account = entity.get_all_accounts().filter( - coa_model=coa, - code=account_data["code"] - ) - - if existing_account: + # 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"Creating account: {account_data['code']}") - account = entity.create_account( - coa_model=coa, + 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"]), + balance_type=account_data["balance_type"], active=True, ) - logger.info(f"Successfully created account: {account_data['code']}") - + logger.info(f"Created account: {account}") if account: - account.role_default = account_data["default"] - account.save() - logger.info(f"Successfully created account: {account_data['code']}") + account.role_default = account_data.get("default", False) + account.save(update_fields=['role_default']) return True + except IntegrityError: - logger.warning(f"Account {account_data['code']} already exists (IntegrityError)") - return True + return True # Already created by race condition except Exception as e: - logger.error(f"Error creating account {account_data['code']}: {e}") - return False + logger.error(f"❌ Error creating {account_data['code']}: {e}") return False + # def create_account(entity, coa, account_data): # try: # account = entity.create_account(