Skip to content

Conversation

@vigneshhari
Copy link
Member

@vigneshhari vigneshhari commented Nov 1, 2025

Odoo

Summary by CodeRabbit

  • New Features

    • Odoo integration: automatic syncing of invoices, payments, delivery orders, products, categories, partners, and users; user-agent mapping and provisioning.
    • New post-create behavior: newly created users without passwords receive a password-reset email.
  • Improvements

    • Increased transactional safety for create/update operations to improve data consistency.
    • Startup wiring and real-time hooks to trigger Odoo synchronizations; structured API payloads and end-to-end sync workflows.
  • Chore

    • Added Odoo configuration, connector dependency, and planning TODOs.

vigneshhari and others added 30 commits July 14, 2025 21:51
* UserOdooAgent Model

* Move User viewset changes to signals

* Remove comment
* Odoo Requester for appointment

* odoo dispense
@vigneshhari vigneshhari requested a review from a team as a code owner November 1, 2025 10:04
headers_str = " ".join([f"-H '{k}: {v}'" for k, v in headers.items()])
data_str = f"-d '{json.dumps(data)}'" if data else ""
curl_command = f"curl -X POST {headers_str} {data_str} '{url}'"
logger.info("Equivalent curl command:\n%s", curl_command)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.

Copilot Autofix

AI 3 months ago

To address the issue, the logging of sensitive Authorization header values should be omitted, redacted, or masked. The best way is to construct the curl command without the actual credentials in the Authorization header—either by omitting the header entirely or by replacing its value with a placeholder such as <REDACTED>.

Specifically:

  • In the block that constructs headers_str, we should detect the Authorization header and replace its value with <REDACTED> or [REDACTED].
  • The rest of the curl command construction remains unchanged so that debugging remains useful while sensitive information is protected.
  • This change only concerns lines within the method shown, especially lines 48–51.

No changes to the rest of the functionality are required. No additional imports are necessary.

Suggested changeset 1
odoo/connector/connector.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/odoo/connector/connector.py b/odoo/connector/connector.py
--- a/odoo/connector/connector.py
+++ b/odoo/connector/connector.py
@@ -45,7 +45,10 @@
 
         # Log curl equivalent for debugging
         try:
-            headers_str = " ".join([f"-H '{k}: {v}'" for k, v in headers.items()])
+            headers_str = " ".join([
+                f"-H '{k}: {('[REDACTED]' if k.lower() == 'authorization' else v)}'"
+                for k, v in headers.items()
+            ])
             data_str = f"-d '{json.dumps(data)}'" if data else ""
             curl_command = f"curl -X POST {headers_str} {data_str} '{url}'"
             logger.info("Equivalent curl command:\n%s", curl_command)
EOF
@@ -45,7 +45,10 @@

# Log curl equivalent for debugging
try:
headers_str = " ".join([f"-H '{k}: {v}'" for k, v in headers.items()])
headers_str = " ".join([
f"-H '{k}: {('[REDACTED]' if k.lower() == 'authorization' else v)}'"
for k, v in headers.items()
])
data_str = f"-d '{json.dumps(data)}'" if data else ""
curl_command = f"curl -X POST {headers_str} {data_str} '{url}'"
logger.info("Equivalent curl command:\n%s", curl_command)
Copilot is powered by AI and may make mistakes. Always verify output.
return response_json
except requests.exceptions.RequestException as e:
logger.exception("Odoo API Error: %s", str(e))
raise ValidationError(str(e)) from e

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 3 months ago

To fix this problem, we should avoid exposing the raw content of exceptions from the requests library to end users, since these can contain sensitive information about system internals, URLs, network errors, etc. Instead, we will log the full details server-side for debugging, and raise a ValidationError with a fixed, generic error message (such as "Failed to contact Odoo server") to the API client.

Specifically, in odoo/connector/connector.py, inside the except requests.exceptions.RequestException as e: handler (lines 69-71), we should replace the raised ValidationError(str(e)) with a generic message, such as ValidationError("Failed to contact Odoo server. Please try again later."). The detailed exception is already logged with logger.exception, fulfilling the requirement to capture the stack trace for developers.

There are no new methods, imports, or definitions needed.


Suggested changeset 1
odoo/connector/connector.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/odoo/connector/connector.py b/odoo/connector/connector.py
--- a/odoo/connector/connector.py
+++ b/odoo/connector/connector.py
@@ -68,4 +68,4 @@
             return response_json
         except requests.exceptions.RequestException as e:
             logger.exception("Odoo API Resonse Processing Error: %s", str(e))
-            raise ValidationError(str(e)) from e
+            raise ValidationError("Failed to contact Odoo server. Please try again later.") from e
EOF
@@ -68,4 +68,4 @@
return response_json
except requests.exceptions.RequestException as e:
logger.exception("Odoo API Resonse Processing Error: %s", str(e))
raise ValidationError(str(e)) from e
raise ValidationError("Failed to contact Odoo server. Please try again later.") from e
Copilot is powered by AI and may make mistakes. Always verify output.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 10, 2025

📝 Walkthrough

Walkthrough

Adds an Odoo integration: new connector, Pydantic specs, resources to sync products/partners/users/invoices/payments/categories/delivery orders, signal handlers, AppConfig and model for user↔Odoo agent, ODOO_CONFIG setting, Pipfile addition, and transaction atomicity wrappers in multiple EMR viewsets.

Changes

Cohort / File(s) Summary
Transaction management in EMR viewsets
care/emr/api/viewsets/charge_item_definition.py, care/emr/api/viewsets/inventory/delivery_order.py, care/emr/api/viewsets/inventory/product.py, care/emr/api/viewsets/invoice.py, care/emr/api/viewsets/organization.py, care/emr/api/viewsets/payment_reconciliation.py, care/emr/api/viewsets/resource_category.py, care/emr/api/viewsets/user.py
Imported django.db.transaction and wrapped perform_create / perform_update calls in with transaction.atomic(): blocks; UserViewSet.perform_create also attempts to send a password-reset email and raises to trigger rollback on send failure.
Pipfile & settings
Pipfile, config/settings/base.py
Adds odoorpc==0.10.1 to Pipfile; adds ODOO_CONFIG dict in config/settings/base.py populated from environment variables.
Django app bootstrap & TODO
odoo/apps.py, odoo/TODO.md
Adds OdooConfig AppConfig that imports odoo.signals on ready and adds odoo/TODO.md with integration planning notes.
Connector & user mapping model
odoo/connector/connector.py, odoo/models.py
Adds OdooConnector.call_api() to build/auth POST requests to Odoo and a UserOdooAgent model mapping local users to Odoo agent IDs.
Account-move specs & resources
odoo/resource/account_move/spec.py, odoo/resource/account_move/invoice.py, odoo/resource/account_move/delivery_order.py
Adds Pydantic models (discounts, AgentData, InvoiceItem, AccountMoveApiRequest/Return) and resources to assemble and POST invoice/vendor-bill payloads (invoice sync, invoice return, delivery-order sync).
Payment specs & resources
odoo/resource/account_move_payment/spec.py, odoo/resource/account_move_payment/payment.py
Adds payment enums/models and OdooPaymentResource methods to sync payments and payment cancellations to Odoo.
Product category spec & resource
odoo/resource/product_category/spec.py, odoo/resource/product_category/category.py
Adds CategoryData model and OdooCategoryResource.sync_category_to_odoo_api() to post categories (including parent relation).
Product spec & resource
odoo/resource/product_product/spec.py, odoo/resource/product_product/resource.py
Adds TaxData, ProductStatus, ProductData models and OdooProductProductResource to extract pricing/taxes/category and sync charge-item/product data to Odoo.
Partner spec & resource
odoo/resource/res_partner/spec.py, odoo/resource/res_partner/resource.py
Adds PartnerType enum and PartnerData model and OdooPartnerResource.sync_partner_to_odoo_api() to create partners from Organization data.
User spec & resource
odoo/resource/res_user/spec.py, odoo/resource/res_user/resource.py
Adds UserType and UserData models and OdooUserResource (with full-name helper) and sync logic for users.
Resourcing & shared specs
odoo/resource/...
Introduces multiple Pydantic spec modules referenced by resources to standardize payloads across API calls (category, product, partner, user, account-move, payments).
Signal handlers
odoo/signals.py
Adds post_save receivers for User, Invoice, PaymentReconciliation, ChargeItemDefinition, ResourceCategory, Organization, DeliveryOrder, Product (and related models) to trigger corresponding Odoo syncs based on state/creation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Files needing extra scrutiny:
    • odoo/connector/connector.py — auth header construction, URL/port composition, and logging that may expose secrets.
    • odoo/resource/account_move/invoice.py, odoo/resource/account_move/delivery_order.py, odoo/resource/product_product/resource.py — dense data-mapping and transformation logic; ensure required fields and error paths handled.
    • odoo/signals.py — post_save semantics: avoid duplicate or premature syncs and inspect conditions for update vs create.
    • EMR viewset transaction wrappers — verify correct return/exception semantics and that moved super() calls preserve behavior.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is extremely sparse—only the word 'Odoo' is provided—and does not follow the repository's description template structure. Complete the description using the template: add a 'Proposed Changes' section summarizing the changes, link any associated issues, document architecture changes, and include the merge checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 44.90% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Amjith's Odoo' is vague and doesn't clearly describe the main changes; it appears to be a person's name rather than a meaningful summary of the work. Consider using a more descriptive title that summarizes the primary changes, such as 'Add Odoo integration module for syncing invoices, payments, and products' or 'Implement Odoo API connector and synchronization workflows'.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch amjith/odoo_module

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
care/emr/api/viewsets/user.py (1)

109-116: Email failure should not rollback user creation.

Raising IntegrityError when password reset email fails is semantically incorrect (it's not a data integrity issue) and creates a poor user experience. If the email service is temporarily down, admins cannot create users at all. Additionally, the base class's transaction.atomic() will catch this and rollback the entire user creation.

Consider decoupling email sending from user creation:

-            if not instance.has_usable_password():
-                try:
-                    mail_type = MailTypeChoices.create.value
-                    send_password_reset_email(instance, mail_type)
-                except Exception as e:
-                    raise IntegrityError(
-                        "User creation failed due to email error."
-                    ) from e  # to fail the transaction
+        # Move outside transaction - call after super().perform_create() completes
+        if not instance.has_usable_password():
+            try:
+                mail_type = MailTypeChoices.create.value
+                send_password_reset_email(instance, mail_type)
+            except Exception as e:
+                # Log error but don't fail user creation
+                logger.error(
+                    f"Failed to send password reset email for user {instance.username}: {e}"
+                )
+                # Optionally: Queue for retry via async task

Alternatively, use Celery to send the email asynchronously after user creation succeeds, ensuring resilience against transient email service failures.

♻️ Duplicate comments (2)
odoo/connector/connector.py (2)

46-53: Critical: Password logged in clear text.

Line 51 logs the complete curl command including the Authorization header with the password. This is a critical security vulnerability as passwords will appear in application logs, potentially exposing credentials to anyone with log access.

Remove or redact the Authorization header before logging:

         # Log curl equivalent for debugging
         try:
+            safe_headers = headers.copy()
+            safe_headers["Authorization"] = "Basic [REDACTED]"
-            headers_str = " ".join([f"-H '{k}: {v}'" for k, v in headers.items()])
+            headers_str = " ".join([f"-H '{k}: {v}'" for k, v in safe_headers.items()])
             data_str = f"-d '{json.dumps(data)}'" if data else ""
             curl_command = f"curl -X POST {headers_str} {data_str} '{url}'"
             logger.info("Equivalent curl command:\n%s", curl_command)
         except Exception as e:
             logger.info(e)

69-71: Exception details exposed to users.

Line 71 includes str(e) in the ValidationError, which can expose stack traces and internal error details to API consumers. This is a security concern as it may leak implementation details.

Provide a generic error message to users:

         except requests.exceptions.RequestException as e:
-            logger.exception("Odoo API Resonse Processing Error: %s", str(e))
-            raise ValidationError(str(e)) from e
+            logger.exception("Odoo API Response Processing Error")
+            raise ValidationError("Failed to communicate with Odoo service") from e
🧹 Nitpick comments (25)
odoo/TODO.md (1)

1-15: Consider moving TODO items to a dedicated issue tracker rather than committing to version control.

While it's helpful to document these planning items, committing a TODO file to the repository is generally discouraged. These items would be better tracked in GitHub Issues, a project board, or a project management tool, where they can be assigned, prioritized, linked to PRs, and tracked over time.

That said, if these items must remain in the repository, consider the following improvements:

  • Some items are phrased as open-ended questions ("Figure out...", "Checkout how...") rather than actionable tasks. Rephrase for clarity.
  • Add priority levels (High/Medium/Low) or dependencies between items to clarify sequencing.
  • Consider grouping related items (e.g., accounting-related tasks together).
  • Add ownership or estimated effort where possible.

Example of a more structured approach:

# TODO Items

## Configuration & Setup
- [ ] Make country, currency and other settings configurable (High priority)
- [ ] Automate initial changes required as migrations if possible or as a script

## Data Mapping & Accounting
- [ ] Store care id in invoice and use it when syncing
- [ ] Understand the difference between product template and product variant
- [ ] Map charge item accounts to Odoo accounts
- [ ] Determine account mappings for discounts
- [ ] Determine account mappings for taxes
- [ ] Validate and apply rounding logic consistently

## Sync Operations
- [ ] Implement invoice cancellation sync (keep both systems in sync)
- [ ] Implement transaction handling in Odoo
- [ ] Implement exception handling strategy
- [ ] Create command/periodic task to sync invoices, products, and users

## Investigation & Documentation
- [ ] Document accounting reporting and account mapping strategy
care/emr/api/viewsets/inventory/product.py (2)

62-63: Redundant atomic block: base class already provides transaction safety.

The perform_create in the base class (EMRBaseViewSet via EMRCreateMixin) already wraps operations in transaction.atomic() (see care/emr/api/viewsets/base.py lines 94-124). This nested atomic block is redundant—it simply becomes a savepoint with no additional benefit.

Consider removing the explicit atomic wrapper here since the base class already guarantees atomicity:

-        with transaction.atomic():
-            super().perform_create(instance)
+        super().perform_create(instance)

66-67: Redundant atomic block: base class already provides transaction safety.

The perform_update in the base class already wraps operations in transaction.atomic() (see care/emr/api/viewsets/base.py lines 176-195). This nested atomic block adds no value.

Consider removing this wrapper:

-        with transaction.atomic():
-            super().perform_update(instance)
+        super().perform_update(instance)
care/emr/api/viewsets/payment_reconciliation.py (2)

80-81: Redundant atomic block: base class already provides transaction safety.

The base class's perform_create already executes within a transaction.atomic() block (care/emr/api/viewsets/base.py lines 94-124), making this wrapper unnecessary.

-        with transaction.atomic():
-            super().perform_create(instance)
+        super().perform_create(instance)

93-94: Redundant atomic block: base class already provides transaction safety.

The base class's perform_update already wraps operations in transaction.atomic().

-        with transaction.atomic():
-            super().perform_update(instance)
+        super().perform_update(instance)
care/emr/api/viewsets/charge_item_definition.py (2)

91-92: Redundant atomic block: base class already provides transaction safety.

The base class perform_create already wraps the save operation in transaction.atomic() (care/emr/api/viewsets/base.py lines 94-124).

-        with transaction.atomic():
-            super().perform_create(instance)
+        super().perform_create(instance)

98-99: Redundant atomic block: base class already provides transaction safety.

The base class perform_update already ensures atomicity.

-        with transaction.atomic():
-            super().perform_update(instance)
+        super().perform_update(instance)
care/emr/api/viewsets/invoice.py (1)

134-172: Redundant atomic block: base class already provides transaction safety.

While the InvoiceLock context manager provides invoice-specific locking, the nested transaction.atomic() block is redundant since the base class's perform_update already wraps operations atomically (care/emr/api/viewsets/base.py lines 176-195). The lock and atomic semantics are independent—you could keep the lock without the explicit atomic wrapper.

Consider simplifying:

     def perform_update(self, instance):
         with InvoiceLock(instance):
-            with transaction.atomic():
-                old_invoice = Invoice.objects.get(id=instance.id)
-                if old_invoice.status != instance.status:
-                    if instance.status in INVOICE_CANCELLED_STATUS:
-                        raise ValidationError(
-                            "Call the cancel invoice API to cancel the invoice"
-                        )
-                    # ... rest of validation logic ...
-                super().perform_update(instance)
+            old_invoice = Invoice.objects.get(id=instance.id)
+            if old_invoice.status != instance.status:
+                if instance.status in INVOICE_CANCELLED_STATUS:
+                    raise ValidationError(
+                        "Call the cancel invoice API to cancel the invoice"
+                    )
+                # ... rest of validation logic ...
+            super().perform_update(instance)
             rebalance_account_task.delay(instance.account.id)
         return instance
odoo/models.py (1)

17-17: Consider adding a database index and uniqueness constraint.

The odoo_agent_id field will likely be used for lookups when syncing data back from Odoo. Without a database index, these queries will perform full table scans as the table grows. Additionally, if each Odoo agent maps to exactly one Care user, consider adding a uniqueness constraint to prevent duplicate mappings.

-    odoo_agent_id = models.IntegerField(help_text="The ID of the agent in Odoo system")
+    odoo_agent_id = models.IntegerField(
+        help_text="The ID of the agent in Odoo system",
+        db_index=True,
+        unique=True,  # Add only if one-to-one mapping is required on Odoo side
+    )

If Odoo agents can map to multiple Care users, omit unique=True but keep db_index=True.

care/emr/api/viewsets/user.py (1)

85-107: Redundant atomic block: base class already provides transaction safety.

The base class's perform_create already wraps operations in transaction.atomic() (care/emr/api/viewsets/base.py lines 94-124), making this explicit wrapper unnecessary.

     def perform_create(self, instance):
-        with transaction.atomic():
-            super().perform_create(instance)
-            # Get or create organization with the role
-            org_name = instance.user_type.capitalize()
-            org = Organization.objects.filter(
-                parent__isnull=True,
-                name=org_name,
-                org_type="role",
-                system_generated=True,
-            ).first()
-            if not org:
-                org = Organization.objects.create(
-                    name=org_name, org_type="role", system_generated=True
-                )
-            # Add User to organization with default role
-            OrganizationUser.objects.create(
-                organization=org,
-                user=instance,
-                role=RoleModel.objects.get(
-                    is_system=True,
-                    name=UserTypeRoleMapping[instance.user_type].value.name,
-                ),
-            )
-
-            if not instance.has_usable_password():
-                try:
-                    mail_type = MailTypeChoices.create.value
-                    send_password_reset_email(instance, mail_type)
-                except Exception as e:
-                    raise IntegrityError(
-                        "User creation failed due to email error."
-                    ) from e  # to fail the transaction
+        super().perform_create(instance)
+        # Get or create organization with the role
+        org_name = instance.user_type.capitalize()
+        org = Organization.objects.filter(
+            parent__isnull=True,
+            name=org_name,
+            org_type="role",
+            system_generated=True,
+        ).first()
+        if not org:
+            org = Organization.objects.create(
+                name=org_name, org_type="role", system_generated=True
+            )
+        # Add User to organization with default role
+        OrganizationUser.objects.create(
+            organization=org,
+            user=instance,
+            role=RoleModel.objects.get(
+                is_system=True,
+                name=UserTypeRoleMapping[instance.user_type].value.name,
+            ),
+        )
+
+        if not instance.has_usable_password():
+            try:
+                mail_type = MailTypeChoices.create.value
+                send_password_reset_email(instance, mail_type)
+            except Exception as e:
+                raise IntegrityError(
+                    "User creation failed due to email error."
+                ) from e  # to fail the transaction

Though honestly, the email error handling has deeper issues (see next comment).

care/emr/api/viewsets/resource_category.py (2)

95-96: Redundant atomic block: base class already provides transaction safety.

The base class's perform_create already wraps operations in transaction.atomic() (care/emr/api/viewsets/base.py lines 94-124).

-        with transaction.atomic():
-            super().perform_create(instance)
+        super().perform_create(instance)

102-103: Redundant atomic block: base class already provides transaction safety.

The base class's perform_update already ensures atomicity (care/emr/api/viewsets/base.py lines 176-195).

-        with transaction.atomic():
-            super().perform_update(instance)
+        super().perform_update(instance)
odoo/resource/product_category/spec.py (1)

4-7: Consider adding validation for UUID fields.

The parent_x_care_id and x_care_id fields appear to hold UUID values based on their usage in odoo/resource/product_category/category.py. Adding Pydantic field validation would help catch invalid data early. Also, parent_x_care_id is set to an empty string when there's no parent—perhaps Optional[str] would better express this intent.

Example validation approach:

+from pydantic import BaseModel, Field
+from typing import Optional

 class CategoryData(BaseModel):
     category_name: str
-    parent_x_care_id: str
-    x_care_id: str
+    parent_x_care_id: Optional[str] = Field(None, description="Parent category UUID or None")
+    x_care_id: str = Field(..., description="Category UUID")
odoo/resource/res_user/spec.py (1)

13-20: Consider adding field validation for email and phone.

The email and phone fields lack format validation. Pydantic's EmailStr type could ensure email validity, and a regex pattern could validate phone numbers. This would catch data quality issues before sending to Odoo.

-from pydantic import BaseModel
+from pydantic import BaseModel, EmailStr, Field

 class UserData(BaseModel):
     name: str
     login: str
-    email: str
+    email: EmailStr
     user_type: UserType
-    phone: str
+    phone: str = Field(..., pattern=r"^\+?[0-9\s\-\(\)]+$")
     state: str
     partner_data: PartnerData
odoo/resource/res_partner/resource.py (1)

7-34: Missing error handling for API failures.

The method doesn't handle exceptions from OdooConnector.call_api, which can raise ValidationError per odoo/connector/connector.py lines 69-71. If the API call fails or returns an unexpected response structure, the sync silently returns None. Callers might not know whether the sync succeeded or failed.

Consider logging errors and potentially re-raising them:

+import logging
+
+logger = logging.getLogger(__name__)
+
 class OdooPartnerResource:
     def sync_partner_to_odoo_api(self, organization: Organization) -> int | None:
         """
         Synchronize an organization to Odoo as a partner.

         Args:
             organization: Organization instance

         Returns:
             Odoo partner ID if successful, None otherwise
         """
         # Extract contact information from metadata
         metadata = organization.metadata or {}
         email = metadata.get("email", "")
         phone = metadata.get("phone", "")
         state = metadata.get("state", "kerala")

         data = PartnerData(
             name=organization.name,
             x_care_id=str(organization.external_id),
             email=email,
             phone=phone,
             state=state,
             partner_type=PartnerType.company,
             agent=False,
         ).model_dump()

-        response = OdooConnector.call_api("api/add/partner", data)
-        return response.get("partner", {}).get("id")
+        try:
+            response = OdooConnector.call_api("api/add/partner", data)
+            partner_id = response.get("partner", {}).get("id")
+            if partner_id is None:
+                logger.warning("Odoo sync returned no partner ID for org %s", organization.external_id)
+            return partner_id
+        except Exception as e:
+            logger.exception("Failed to sync partner to Odoo for org %s", organization.external_id)
+            return None
odoo/apps.py (1)

9-10: Specify the exact error code in the noqa directive.

The blanket # noqa is flagged by Ruff. Since you're importing for side effects (signal registration), use # noqa: F401 to specifically suppress the "unused import" warning. This makes the intent clearer.

     def ready(self):
-        import odoo.signals  # noqa
+        import odoo.signals  # noqa: F401
odoo/resource/product_category/category.py (1)

6-25: Missing error handling for API failures.

Like the partner resource, this method doesn't handle exceptions from OdooConnector.call_api. Silent failures make debugging difficult and leave callers uncertain about sync status. Consider adding try-except blocks and logging as shown in the partner resource review.

odoo/connector/connector.py (2)

52-53: Avoid catching bare Exception.

Line 52 catches all exceptions with Exception, which can hide unexpected errors. If the curl logging fails, it's probably acceptable to skip it, but catching specific exceptions like TypeError or ValueError would be more precise.

-        except Exception as e:
-            logger.info(e)
+        except (TypeError, ValueError) as e:
+            logger.debug("Failed to generate curl command: %s", e)

68-71: Follow try-else pattern for clarity.

The return response_json statement (Line 68) should be in an else block to make the control flow explicit: successful responses return, errors raise. Also, logger.exception already includes exception info, so passing str(e) to the message is redundant (Line 70).

         try:
             response = requests.post(url, headers=headers, json=data, timeout=30)
             logger.info("Odoo API Response Status: %s", response.status_code)
             logger.info("Odoo API Raw Response: %s", response.text)

             response_json = response.json()
             logger.info("Odoo API Response JSON: %s", response_json)

             if not response.ok:
                 error_msg = response_json.get("message", str(response.reason))
                 logger.exception("Odoo API Response Error: %s", error_msg)
                 response.raise_for_status()  # This will raise HTTPError with proper status code
-
-            return response_json
+            else:
+                return response_json
         except requests.exceptions.RequestException as e:
-            logger.exception("Odoo API Resonse Processing Error: %s", str(e))
+            logger.exception("Odoo API Response Processing Error")
             raise ValidationError("Failed to communicate with Odoo service") from e

Note: Also fixed the typo "Resonse" → "Response" in the log message.

odoo/resource/res_user/resource.py (1)

13-46: Consider deriving state from user model and add error handling.

A few observations that might warrant attention:

  1. The state is hardcoded to "kerala" (lines 29, 41), but the User model has state, district, and local_body fields that could provide the actual location.
  2. The API call lacks error handling, so Odoo failures would propagate as unhandled exceptions and potentially roll back the user save transaction.
  3. The method is called on every save via signals (not just creation), which could cause duplicate API calls.

Consider this approach:

     def sync_user_to_odoo_api(self, user) -> int | None:
         """
         Synchronize a user to Odoo.
 
         Args:
             user: User instance
 
         Returns:
             Odoo user ID if successful, None otherwise
         """
+        try:
+            state = user.state.name.lower() if user.state else "kerala"
+        except AttributeError:
+            state = "kerala"
+
         # Create partner data first
         partner_data = PartnerData(
             name=self.get_full_name(user),
             x_care_id=str(user.external_id),
             partner_type=PartnerType.person,
             phone=user.phone_number,
-            state="kerala",  # Default to Kerala
+            state=state,
             email=user.email,
             agent=True,
         )
 
         # Create user data
         data = UserData(
             name=self.get_full_name(user),
             login=user.username,
             email=user.email,
             user_type=UserType.internal,  # Default to internal user
             phone=user.phone_number,
-            state="kerala",  # Default to Kerala
+            state=state,
             partner_data=partner_data,
         ).model_dump()
 
-        response = OdooConnector.call_api("api/add/user", data)
-        return response.get("user", {}).get("id")
+        try:
+            response = OdooConnector.call_api("api/add/user", data)
+            return response.get("user", {}).get("id")
+        except Exception as e:
+            logger.exception("Failed to sync user %s to Odoo: %s", user.external_id, e)
+            return None
odoo/signals.py (2)

72-79: Consider adding conditional logic to avoid unnecessary syncs.

The handler fires on every ChargeItemDefinition save without checking if relevant fields changed. This could result in redundant Odoo API calls.

Consider tracking whether sync-relevant fields changed:

if created or (update_fields and any(field in update_fields for field in ['title', 'price_components', 'category'])):
    # sync to Odoo

117-124: Consider adding null check and transaction handling.

The handler calls sync_product_from_product_model, which returns None if the product lacks a charge_item_definition. Consider adding a conditional check here to avoid the function call entirely.

 @receiver(post_save, sender=Product)
 def sync_product_to_odoo(sender, instance, created, **kwargs):
     """
     Signal handler to sync product to Odoo when it has a charge item definition.
     """
+    if not instance.charge_item_definition:
+        return
     odoo_product = OdooProductProductResource()
     odoo_product.sync_product_from_product_model(instance)
odoo/resource/product_product/resource.py (1)

86-106: LGTM with a minor suggestion.

The early return for missing charge_item_definition is appropriate. Consider adding a similar null check for product_knowledge before accessing its alternate_identifier.

         hsn = (
-            product.product_knowledge.alternate_identifier
-            if product.product_knowledge
-            and product.product_knowledge.alternate_identifier
+            product.product_knowledge.alternate_identifier or ""
+            if product.product_knowledge
             else ""
         )
odoo/resource/account_move/spec.py (2)

26-33: Consider type improvements for dates and reason field.

A few observations on the API design:

  1. invoice_date and due_date are strings rather than date types, which means callers must manually format dates and lose type safety
  2. The reason field is required but both usages in the codebase pass an empty string "", suggesting it should be optional with a default value

Consider these refinements:

+from datetime import date
+
 class AccountMoveApiRequest(BaseModel):
     x_care_id: str
     bill_type: BillType
-    invoice_date: str
-    due_date: str
+    invoice_date: date | str  # Accept both for flexibility
+    due_date: date | str
     partner_data: PartnerData
     invoice_items: list[InvoiceItem]
-    reason: str
+    reason: str = ""  # Optional with default

Though if the Odoo API specifically expects string dates in "dd-mm-yyyy" format, the current approach is reasonable.


9-10: Remove the unused AgentData class.

The verification confirms AgentData is not referenced anywhere in the codebase. Since agent_id in InvoiceItem is already a plain string field, the AgentData class serves no purpose and should be removed from lines 9-10.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ea179e and 86717a0.

⛔ Files ignored due to path filters (1)
  • Pipfile.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • Pipfile (1 hunks)
  • care/emr/api/viewsets/charge_item_definition.py (2 hunks)
  • care/emr/api/viewsets/inventory/delivery_order.py (2 hunks)
  • care/emr/api/viewsets/inventory/product.py (2 hunks)
  • care/emr/api/viewsets/invoice.py (1 hunks)
  • care/emr/api/viewsets/organization.py (1 hunks)
  • care/emr/api/viewsets/payment_reconciliation.py (3 hunks)
  • care/emr/api/viewsets/resource_category.py (2 hunks)
  • care/emr/api/viewsets/user.py (1 hunks)
  • config/settings/base.py (2 hunks)
  • odoo/TODO.md (1 hunks)
  • odoo/apps.py (1 hunks)
  • odoo/connector/connector.py (1 hunks)
  • odoo/models.py (1 hunks)
  • odoo/resource/account_move/delivery_order.py (1 hunks)
  • odoo/resource/account_move/invoice.py (1 hunks)
  • odoo/resource/account_move/spec.py (1 hunks)
  • odoo/resource/account_move_payment/payment.py (1 hunks)
  • odoo/resource/account_move_payment/spec.py (1 hunks)
  • odoo/resource/product_category/category.py (1 hunks)
  • odoo/resource/product_category/spec.py (1 hunks)
  • odoo/resource/product_product/resource.py (1 hunks)
  • odoo/resource/product_product/spec.py (1 hunks)
  • odoo/resource/res_partner/resource.py (1 hunks)
  • odoo/resource/res_partner/spec.py (1 hunks)
  • odoo/resource/res_user/resource.py (1 hunks)
  • odoo/resource/res_user/spec.py (1 hunks)
  • odoo/signals.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.py

📄 CodeRabbit inference engine (.cursorrules)

**/*.py: Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance).
Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables).

Files:

  • care/emr/api/viewsets/charge_item_definition.py
  • odoo/apps.py
  • odoo/resource/res_partner/spec.py
  • config/settings/base.py
  • odoo/resource/res_user/spec.py
  • odoo/connector/connector.py
  • odoo/resource/account_move_payment/payment.py
  • odoo/resource/res_partner/resource.py
  • odoo/resource/product_product/spec.py
  • care/emr/api/viewsets/invoice.py
  • odoo/resource/product_product/resource.py
  • care/emr/api/viewsets/resource_category.py
  • odoo/resource/product_category/spec.py
  • odoo/signals.py
  • care/emr/api/viewsets/inventory/product.py
  • odoo/resource/product_category/category.py
  • odoo/resource/account_move/delivery_order.py
  • care/emr/api/viewsets/organization.py
  • odoo/resource/account_move_payment/spec.py
  • odoo/resource/account_move/invoice.py
  • care/emr/api/viewsets/user.py
  • care/emr/api/viewsets/payment_reconciliation.py
  • care/emr/api/viewsets/inventory/delivery_order.py
  • odoo/resource/res_user/resource.py
  • odoo/models.py
  • odoo/resource/account_move/spec.py
**/signals.py

📄 CodeRabbit inference engine (.cursorrules)

Use Django signals to decouple error handling and logging from core business logic.

Files:

  • odoo/signals.py
**/{models,views,forms}.py

📄 CodeRabbit inference engine (.cursorrules)

Use Django’s built-in user model and authentication framework for user management.

Files:

  • odoo/models.py
**/{forms,models}.py

📄 CodeRabbit inference engine (.cursorrules)

Use Django's validation framework to validate form and model data.

Files:

  • odoo/models.py
**/{views,models,tasks}.py

📄 CodeRabbit inference engine (.cursorrules)

Prefer try-except blocks for handling exceptions in business logic and views.

Files:

  • odoo/models.py
**/{models,forms,views}.py

📄 CodeRabbit inference engine (.cursorrules)

Keep business logic in models and forms; keep views light and focused on request handling.

Files:

  • odoo/models.py
**/{views,models}*.py

📄 CodeRabbit inference engine (.cursorrules)

**/{views,models}*.py: Leverage Django’s caching framework to optimize performance for frequently accessed data.
Use Django’s cache framework with backend support (e.g., Redis or Memcached) to reduce database load.

Files:

  • odoo/models.py
**/{views,models}.py

📄 CodeRabbit inference engine (.cursorrules)

Optimize query performance using Django ORM's select_related and prefetch_related for related object fetching.

Files:

  • odoo/models.py
**/models.py

📄 CodeRabbit inference engine (.cursorrules)

Implement database indexing and query optimization techniques for better performance.

Files:

  • odoo/models.py
🧠 Learnings (2)
📚 Learning: 2025-07-18T15:17:40.483Z
Learnt from: CR
Repo: ohcnetwork/care PR: 0
File: .cursorrules:0-0
Timestamp: 2025-07-18T15:17:40.483Z
Learning: Applies to **/signals.py : Use Django signals to decouple error handling and logging from core business logic.

Applied to files:

  • odoo/signals.py
📚 Learning: 2024-11-28T06:13:52.067Z
Learnt from: DraKen0009
Repo: ohcnetwork/care PR: 2620
File: care/facility/models/bed.py:62-65
Timestamp: 2024-11-28T06:13:52.067Z
Learning: In `care/facility/models/bed.py`, when deleting a `Bed` instance in the `delete` method, if we're only adding validation and performing a single update, adding an atomic transaction is not necessary.

Applied to files:

  • care/emr/api/viewsets/inventory/delivery_order.py
🧬 Code graph analysis (20)
care/emr/api/viewsets/charge_item_definition.py (5)
care/emr/api/viewsets/inventory/product.py (2)
  • perform_create (49-63)
  • perform_update (65-67)
care/emr/api/viewsets/resource_category.py (2)
  • perform_create (90-96)
  • perform_update (98-103)
care/emr/api/viewsets/base.py (2)
  • perform_create (95-125)
  • perform_update (177-196)
care/emr/models/charge_item_definition.py (1)
  • ChargeItemDefinition (6-24)
care/emr/models/base.py (1)
  • calculate_slug_from_facility (39-40)
odoo/resource/res_user/spec.py (1)
odoo/resource/res_partner/spec.py (1)
  • PartnerData (11-19)
odoo/resource/account_move_payment/payment.py (5)
care/emr/models/payment_reconciliation.py (1)
  • PaymentReconciliation (6-29)
care/emr/resources/payment_reconciliation/spec.py (1)
  • PaymentReconciliationPaymentMethodOptions (48-55)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/account_move_payment/spec.py (6)
  • AccountMovePaymentApiRequest (29-38)
  • AccountPaymentCancelApiRequest (41-43)
  • BillCounterData (23-26)
  • CustomerType (18-20)
  • JournalType (8-10)
  • PaymentMode (13-15)
odoo/resource/res_partner/spec.py (2)
  • PartnerData (11-19)
  • PartnerType (6-8)
odoo/resource/res_partner/resource.py (3)
care/emr/models/organization.py (1)
  • Organization (106-107)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/res_partner/spec.py (2)
  • PartnerData (11-19)
  • PartnerType (6-8)
odoo/resource/product_product/spec.py (1)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
care/emr/api/viewsets/invoice.py (5)
care/emr/models/invoice.py (1)
  • Invoice (7-31)
care/emr/resources/invoice/spec.py (1)
  • InvoiceStatusOptions (15-20)
care/emr/models/charge_item.py (1)
  • ChargeItem (7-47)
care/emr/resources/charge_item/spec.py (1)
  • ChargeItemStatusOptions (20-27)
care/emr/api/viewsets/payment_reconciliation.py (1)
  • perform_update (84-95)
odoo/resource/product_product/resource.py (6)
care/emr/models/charge_item_definition.py (1)
  • ChargeItemDefinition (6-24)
care/emr/models/product.py (1)
  • Product (6-17)
care/emr/resources/common/monetary_component.py (1)
  • MonetaryComponentType (9-14)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/product_product/spec.py (2)
  • ProductData (11-18)
  • TaxData (6-8)
care/emr/api/viewsets/resource_category.py (4)
care/emr/api/viewsets/charge_item_definition.py (2)
  • perform_create (86-92)
  • perform_update (94-99)
care/emr/api/viewsets/base.py (2)
  • perform_create (95-125)
  • perform_update (177-196)
care/emr/models/resource_category.py (1)
  • ResourceCategory (10-86)
care/emr/models/base.py (1)
  • calculate_slug_from_facility (39-40)
odoo/signals.py (19)
care/emr/models/charge_item_definition.py (1)
  • ChargeItemDefinition (6-24)
care/emr/models/invoice.py (1)
  • Invoice (7-31)
care/emr/models/payment_reconciliation.py (1)
  • PaymentReconciliation (6-29)
care/emr/models/product.py (1)
  • Product (6-17)
care/emr/models/resource_category.py (1)
  • ResourceCategory (10-86)
care/emr/models/supply_delivery.py (1)
  • DeliveryOrder (29-48)
care/emr/resources/inventory/supply_delivery/delivery_order.py (1)
  • SupplyDeliveryOrderStatusOptions (18-24)
care/emr/resources/invoice/spec.py (1)
  • InvoiceStatusOptions (15-20)
care/emr/resources/organization/spec.py (1)
  • OrganizationTypeChoices (11-15)
care/emr/resources/payment_reconciliation/spec.py (1)
  • PaymentReconciliationStatusOptions (22-26)
care/emr/resources/resource_category/spec.py (1)
  • ResourceCategoryResourceTypeOptions (10-13)
care/users/models.py (1)
  • User (196-436)
odoo/resource/account_move/delivery_order.py (2)
  • OdooDeliveryOrderResource (21-148)
  • sync_delivery_order_to_odoo_api (46-148)
odoo/resource/account_move/invoice.py (3)
  • OdooInvoiceResource (24-180)
  • sync_invoice_to_odoo_api (51-157)
  • sync_invoice_return_to_odoo_api (159-180)
odoo/resource/account_move_payment/payment.py (3)
  • OdooPaymentResource (21-95)
  • sync_payment_to_odoo_api (22-73)
  • sync_payment_cancel_to_odoo_api (75-95)
odoo/resource/product_category/category.py (2)
  • OdooCategoryResource (5-25)
  • sync_category_to_odoo_api (6-25)
odoo/resource/product_product/resource.py (3)
  • OdooProductProductResource (9-106)
  • sync_product_to_odoo_api (43-84)
  • sync_product_from_product_model (86-106)
odoo/resource/res_partner/resource.py (2)
  • OdooPartnerResource (6-34)
  • sync_partner_to_odoo_api (7-34)
odoo/resource/res_user/resource.py (2)
  • OdooUserResource (7-46)
  • sync_user_to_odoo_api (13-46)
care/emr/api/viewsets/inventory/product.py (3)
care/emr/api/viewsets/charge_item_definition.py (2)
  • perform_create (86-92)
  • perform_update (94-99)
care/emr/api/viewsets/inventory/delivery_order.py (2)
  • perform_create (86-95)
  • perform_update (97-99)
care/emr/api/viewsets/base.py (2)
  • perform_create (95-125)
  • perform_update (177-196)
odoo/resource/product_category/category.py (2)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/account_move/delivery_order.py (8)
care/emr/models/supply_delivery.py (2)
  • DeliveryOrder (29-48)
  • SupplyDelivery (7-26)
care/emr/resources/common/monetary_component.py (1)
  • MonetaryComponentType (9-14)
care/emr/resources/inventory/supply_delivery/spec.py (1)
  • SupplyDeliveryStatusOptions (18-22)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/account_move/spec.py (3)
  • AccountMoveApiRequest (26-33)
  • BillType (21-23)
  • InvoiceItem (13-18)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/product_product/spec.py (1)
  • ProductData (11-18)
odoo/resource/res_partner/spec.py (2)
  • PartnerData (11-19)
  • PartnerType (6-8)
care/emr/api/viewsets/organization.py (2)
care/emr/api/viewsets/user.py (1)
  • perform_create (84-116)
care/emr/api/viewsets/base.py (2)
  • perform_create (95-125)
  • perform_update (177-196)
odoo/resource/account_move_payment/spec.py (1)
odoo/resource/res_partner/spec.py (1)
  • PartnerData (11-19)
odoo/resource/account_move/invoice.py (12)
care/emr/models/charge_item.py (1)
  • ChargeItem (7-47)
care/emr/models/invoice.py (1)
  • Invoice (7-31)
care/emr/models/medication_dispense.py (1)
  • MedicationDispense (8-34)
care/emr/models/scheduling/booking.py (1)
  • TokenBooking (22-50)
care/emr/models/service_request.py (1)
  • ServiceRequest (7-46)
care/emr/resources/charge_item/spec.py (1)
  • ChargeItemResourceOptions (37-41)
care/emr/resources/common/monetary_component.py (1)
  • MonetaryComponentType (9-14)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/account_move/spec.py (4)
  • AccountMoveApiRequest (26-33)
  • AccountMoveReturnApiRequest (36-43)
  • BillType (21-23)
  • InvoiceItem (13-18)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/product_product/spec.py (1)
  • ProductData (11-18)
odoo/resource/res_partner/spec.py (2)
  • PartnerData (11-19)
  • PartnerType (6-8)
care/emr/api/viewsets/payment_reconciliation.py (3)
care/emr/api/viewsets/charge_item_definition.py (2)
  • perform_create (86-92)
  • perform_update (94-99)
care/emr/api/viewsets/inventory/product.py (2)
  • perform_create (49-63)
  • perform_update (65-67)
care/emr/api/viewsets/invoice.py (2)
  • perform_create (91-114)
  • perform_update (132-174)
care/emr/api/viewsets/inventory/delivery_order.py (5)
care/emr/api/viewsets/charge_item_definition.py (2)
  • perform_create (86-92)
  • perform_update (94-99)
care/emr/api/viewsets/inventory/product.py (2)
  • perform_create (49-63)
  • perform_update (65-67)
care/emr/api/viewsets/inventory/request_order.py (1)
  • perform_create (89-97)
care/emr/api/viewsets/inventory/supply_delivery.py (2)
  • perform_create (101-111)
  • perform_update (122-138)
care/emr/api/viewsets/medication_dispense.py (2)
  • perform_create (82-110)
  • perform_update (151-174)
odoo/resource/res_user/resource.py (4)
care/users/models.py (1)
  • User (196-436)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/res_partner/spec.py (2)
  • PartnerData (11-19)
  • PartnerType (6-8)
odoo/resource/res_user/spec.py (2)
  • UserData (13-20)
  • UserType (8-10)
odoo/models.py (1)
odoo/resource/res_user/resource.py (1)
  • get_full_name (8-11)
odoo/resource/account_move/spec.py (2)
odoo/resource/product_product/spec.py (1)
  • ProductData (11-18)
odoo/resource/res_partner/spec.py (1)
  • PartnerData (11-19)
🪛 GitHub Check: CodeQL
odoo/connector/connector.py

[failure] 51-51: Clear-text logging of sensitive information
This expression logs sensitive data (password) as clear text.


[warning] 71-71: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.

🪛 Ruff (0.14.3)
odoo/apps.py

10-10: Unused blanket noqa directive

Remove unused noqa directive

(RUF100)

odoo/connector/connector.py

52-52: Do not catch blind exception: Exception

(BLE001)


68-68: Consider moving this statement to an else block

(TRY300)


70-70: Redundant exception object included in logging.exception call

(TRY401)

care/emr/api/viewsets/invoice.py

138-140: Avoid specifying long messages outside the exception class

(TRY003)


145-145: Avoid specifying long messages outside the exception class

(TRY003)


147-147: Avoid specifying long messages outside the exception class

(TRY003)


152-152: Avoid specifying long messages outside the exception class

(TRY003)


157-159: Avoid specifying long messages outside the exception class

(TRY003)

odoo/resource/product_product/resource.py

14-14: Create your own exception

(TRY002)


14-14: Avoid specifying long messages outside the exception class

(TRY003)

odoo/signals.py

36-36: Unused function argument: sender

(ARG001)


36-36: Unused function argument: created

(ARG001)


36-36: Unused function argument: kwargs

(ARG001)


45-45: Unused function argument: sender

(ARG001)


45-45: Unused function argument: raw

(ARG001)


45-45: Unused function argument: using

(ARG001)


45-45: Unused function argument: update_fields

(ARG001)


45-45: Unused function argument: kwargs

(ARG001)


57-57: Unused function argument: sender

(ARG001)


57-57: Unused function argument: created

(ARG001)


57-57: Unused function argument: kwargs

(ARG001)


73-73: Unused function argument: sender

(ARG001)


73-73: Unused function argument: created

(ARG001)


73-73: Unused function argument: kwargs

(ARG001)


82-82: Unused function argument: sender

(ARG001)


82-82: Unused function argument: created

(ARG001)


82-82: Unused function argument: kwargs

(ARG001)


95-95: Unused function argument: sender

(ARG001)


95-95: Unused function argument: created

(ARG001)


95-95: Unused function argument: kwargs

(ARG001)


105-105: Unused function argument: sender

(ARG001)


105-105: Unused function argument: created

(ARG001)


105-105: Unused function argument: kwargs

(ARG001)


118-118: Unused function argument: sender

(ARG001)


118-118: Unused function argument: created

(ARG001)


118-118: Unused function argument: kwargs

(ARG001)

odoo/resource/account_move/invoice.py

29-29: Create your own exception

(TRY002)


29-29: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test / Test
🔇 Additional comments (16)
care/emr/api/viewsets/payment_reconciliation.py (1)

82-82: Good: Async task triggered after transaction.

The rebalance_account_task.delay() call is correctly placed outside the atomic block, ensuring the task runs only after the transaction commits.

care/emr/api/viewsets/invoice.py (1)

173-173: Good: Async task triggered after transaction.

The rebalance_account_task.delay() call is correctly placed outside the atomic block.

odoo/models.py (1)

11-16: Verify CASCADE deletion behavior.

Using on_delete=models.CASCADE means deleting a user will also delete their Odoo agent mapping. This might be the intended behavior, but consider whether you need to preserve the mapping for audit trails or if you should use on_delete=models.SET_NULL with a nullable foreign key to retain historical records.

Confirm that deleting users should also remove their Odoo agent mappings, or if soft-deletion/preservation is needed.

Pipfile (1)

52-52: No action needed—version is current and secure.

The pinned version 0.10.1 is already the latest stable release on PyPI with no known security vulnerabilities, so the codebase is in good shape.

care/emr/api/viewsets/inventory/delivery_order.py (1)

94-99: Verify if nested transaction wrapping is intentional.

Similar to OrganizationViewSet, wrapping these methods in transaction.atomic() might be redundant if the base class already provides transactional safety. Review the base class implementation to confirm whether this additional wrapping is necessary for Odoo sync signal coordination.

Based on learnings: In simpler cases with single updates, atomic transactions may not be necessary, though here you're calling super() which might perform multiple operations.

odoo/resource/res_partner/spec.py (1)

1-19: LGTM!

The data models are clean and straightforward. The use of Pydantic for validation and the enum for partner type are appropriate choices for external API integration.

config/settings/base.py (1)

142-142: LGTM!

The Odoo app is correctly added to LOCAL_APPS.

odoo/resource/res_user/resource.py (1)

8-11: LGTM!

The full name construction correctly handles optional name components and provides sensible fallbacks.

odoo/resource/product_product/spec.py (1)

1-18: LGTM!

The data models are well-structured and appropriately reference CategoryData. The optional fields for taxes and HSN code provide flexibility for different product types.

odoo/signals.py (3)

81-92: LGTM with transaction caveat.

The conditional check for charge_item_definition resource type is appropriate. However, the same transaction boundary concern applies here as with other handlers.


94-102: LGTM with transaction caveat.

The conditional check for product_supplier org type is appropriate. However, consider applying the transaction.on_commit pattern here as well.


104-115: LGTM with transaction caveat.

The conditional logic checking both status and origin is appropriate for avoiding unnecessary syncs. The same transaction boundary concern applies.

odoo/resource/account_move_payment/spec.py (1)

1-43: LGTM!

The data models are well-structured with appropriate enums and type annotations. The composition of PartnerData and BillCounterData within AccountMovePaymentApiRequest is clean.

odoo/resource/account_move/delivery_order.py (1)

22-30: LGTM on the helper method.

The logic correctly extracts the base price from price components with appropriate fallback handling.

odoo/resource/account_move/spec.py (2)

13-18: String types for quantity and sale_price align with API expectations.

While it might seem unusual to use strings for numeric fields, this matches how the values are prepared in the invoice and delivery_order resources (using str() conversions everywhere). This appears to be an intentional design choice for Odoo API compatibility.


36-43: LGTM on the return request structure.

The design appropriately makes most fields optional since a return/cancellation primarily needs just the care ID and reason. This provides flexibility while keeping the common case simple.

Comment on lines +156 to +162
def perform_create(self, instance):
with transaction.atomic():
super().perform_create(instance)

def perform_update(self, instance):
with transaction.atomic():
super().perform_update(instance)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify if nested transaction wrapping is intentional.

The base class EMRModelViewSet already wraps perform_create and perform_update in transaction.atomic() blocks (see care/emr/api/viewsets/base.py lines 94-124 and 176-195). Adding another transaction.atomic() here creates nested transactions, which Django handles as savepoints. While not harmful, it's likely redundant unless specifically intended for Odoo signal handler isolation.

Run the following script to confirm the base class transaction wrapping:


🏁 Script executed:

#!/bin/bash
# Verify base class transaction handling
ast-grep --pattern $'class EMRModelViewSet($$$) {
  $$$
  def perform_create($$$) {
    $$$
    with transaction.atomic():
      $$$
  }
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for perform_create and perform_update in base viewset with context
rg -n "def perform_create|def perform_update" care/emr/api/viewsets/base.py -A 5 -B 2

Length of output: 740


🏁 Script executed:

#!/bin/bash
# Verify the actual organization.py implementation
cat care/emr/api/viewsets/organization.py | sed -n '150,170p'

Length of output: 688


🏁 Script executed:

#!/bin/bash
# Check if transaction.atomic is imported in both files
rg -n "from django.db import transaction|import transaction" care/emr/api/viewsets/organization.py care/emr/api/viewsets/base.py

Length of output: 139


Remove redundant transaction wrapping in perform_create and perform_update.

The base class methods already wrap operations in transaction.atomic() (base.py lines 98 and 180). Wrapping the super() calls again creates unnecessary nested transactions—Django converts these to savepoints, which adds no atomicity benefit. Simply call super().perform_create(instance) and super().perform_update(instance) directly without the additional with transaction.atomic(): block.

Note: perform_destroy can retain its transaction.atomic() wrapper since it contains custom logic beyond a simple super() call.

🤖 Prompt for AI Agents
In care/emr/api/viewsets/organization.py around lines 156 to 162, the
perform_create and perform_update methods are redundantly wrapping super() calls
in transaction.atomic() even though the base class already provides atomic
coverage; remove the with transaction.atomic(): blocks and call
super().perform_create(instance) and super().perform_update(instance) directly
so you avoid unnecessary nested transactions/savepoints while keeping
perform_destroy unchanged.

Comment on lines +755 to +764
ODOO_CONFIG = {
"host": env("ODOO_HOST", default="host.docker.internal"), # Odoo instance URL
"port": env("ODOO_PORT", default=8069),
"protocol": env("ODOO_PROTOCOL", default="jsonrpc"),
"database": env("ODOO_DATABASE", default="ohc"), # Odoo database name
"username": env("ODOO_USERNAME", default="vignesh@ohc.network"), # Odoo username
"password": env(
"ODOO_PASSWORD", default="9fe380242cf3a95d23b4169ef79167fd4c6640c6"
),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove hardcoded credentials from default values.

The default password value appears to be an actual API token. Even for development environments, hardcoded credentials in source code pose a security risk and could be accidentally committed or exposed.

Apply this diff to remove the sensitive default:

 ODOO_CONFIG = {
     "host": env("ODOO_HOST", default="host.docker.internal"),  # Odoo instance URL
     "port": env("ODOO_PORT", default=8069),
     "protocol": env("ODOO_PROTOCOL", default="jsonrpc"),
     "database": env("ODOO_DATABASE", default="ohc"),  # Odoo database name
-    "username": env("ODOO_USERNAME", default="vignesh@ohc.network"),  # Odoo username
-    "password": env(
-        "ODOO_PASSWORD", default="9fe380242cf3a95d23b4169ef79167fd4c6640c6"
-    ),
+    "username": env("ODOO_USERNAME", default=""),  # Odoo username
+    "password": env("ODOO_PASSWORD", default=""),
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ODOO_CONFIG = {
"host": env("ODOO_HOST", default="host.docker.internal"), # Odoo instance URL
"port": env("ODOO_PORT", default=8069),
"protocol": env("ODOO_PROTOCOL", default="jsonrpc"),
"database": env("ODOO_DATABASE", default="ohc"), # Odoo database name
"username": env("ODOO_USERNAME", default="vignesh@ohc.network"), # Odoo username
"password": env(
"ODOO_PASSWORD", default="9fe380242cf3a95d23b4169ef79167fd4c6640c6"
),
}
ODOO_CONFIG = {
"host": env("ODOO_HOST", default="host.docker.internal"), # Odoo instance URL
"port": env("ODOO_PORT", default=8069),
"protocol": env("ODOO_PROTOCOL", default="jsonrpc"),
"database": env("ODOO_DATABASE", default="ohc"), # Odoo database name
"username": env("ODOO_USERNAME", default=""), # Odoo username
"password": env("ODOO_PASSWORD", default=""),
}
🤖 Prompt for AI Agents
In config/settings/base.py around lines 755 to 764, the ODOO_CONFIG contains a
hardcoded password default which is sensitive; remove the hardcoded default
value for "password" (do not insert any secrets into source), change
env("ODOO_PASSWORD", default="...") to either env("ODOO_PASSWORD") so it becomes
required or env("ODOO_PASSWORD", default="") to default to an empty/placeholder
value, and update any related code or docs to handle missing credentials (fail
fast with a clear error or instruct to set the env var in dev/prod).

Comment on lines +22 to +73
def sync_payment_to_odoo_api(self, payment_id: str) -> int | None:
"""
Synchronize a Django payment reconciliation to Odoo using the custom addon API.

Args:
payment_id: External ID of the Django payment reconciliation

Returns:
Odoo payment ID if successful, None otherwise
"""
payment = PaymentReconciliation.objects.select_related(
"facility", "account", "target_invoice"
).get(external_id=payment_id)

# Prepare partner data
partner_data = PartnerData(
name=payment.account.patient.name,
x_care_id=str(payment.account.patient.external_id),
partner_type=PartnerType.person,
phone=payment.account.patient.phone_number,
state=payment.facility.state or "kerala",
email="",
agent=False,
)

# Prepare payment data
data = AccountMovePaymentApiRequest(
journal_x_care_id=str(
payment.target_invoice.external_id if payment.target_invoice else ""
),
x_care_id=str(payment.external_id),
amount=float(payment.amount),
journal_input=JournalType.cash
if payment.method == PaymentReconciliationPaymentMethodOptions.cash.value
else JournalType.bank,
payment_date=payment.payment_datetime.strftime("%Y-%m-%d"),
payment_mode=PaymentMode.send
if payment.is_credit_note
else PaymentMode.receive,
partner_data=partner_data,
customer_type=CustomerType.customer,
counter_data=BillCounterData(
x_care_id=str(payment.location.external_id),
cashier_id=str(payment.created_by.external_id),
counter_name=payment.location.name,
),
).model_dump()

logger.info("Odoo Payment Data: %s", data)

response = OdooConnector.call_api("api/account/move/payment", data)
return response["payment"]["id"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add null checks for location and created_by.

Several fields accessed without null checks:

  1. Line 64: payment.location.external_id - location is a nullable ForeignKey, so this will raise AttributeError if None.
  2. Line 65: payment.created_by.external_id - though EMRBaseModel likely sets this, it should be verified.
  3. Line 73: Direct dictionary access response["payment"]["id"] could raise KeyError if the response structure differs.

Apply this diff:

     def sync_payment_to_odoo_api(self, payment_id: str) -> int | None:
         """
         Synchronize a Django payment reconciliation to Odoo using the custom addon API.
 
         Args:
             payment_id: External ID of the Django payment reconciliation
 
         Returns:
             Odoo payment ID if successful, None otherwise
         """
         payment = PaymentReconciliation.objects.select_related(
             "facility", "account", "target_invoice"
         ).get(external_id=payment_id)
 
+        if not payment.location or not payment.created_by:
+            logger.warning(
+                "Cannot sync payment %s to Odoo: missing location or creator",
+                payment_id
+            )
+            return None
+
         # Prepare partner data
         partner_data = PartnerData(
             name=payment.account.patient.name,
             x_care_id=str(payment.account.patient.external_id),
             partner_type=PartnerType.person,
             phone=payment.account.patient.phone_number,
             state=payment.facility.state or "kerala",
             email="",
             agent=False,
         )
 
         # Prepare payment data
         data = AccountMovePaymentApiRequest(
             journal_x_care_id=str(
                 payment.target_invoice.external_id if payment.target_invoice else ""
             ),
             x_care_id=str(payment.external_id),
             amount=float(payment.amount),
             journal_input=JournalType.cash
             if payment.method == PaymentReconciliationPaymentMethodOptions.cash.value
             else JournalType.bank,
             payment_date=payment.payment_datetime.strftime("%Y-%m-%d"),
             payment_mode=PaymentMode.send
             if payment.is_credit_note
             else PaymentMode.receive,
             partner_data=partner_data,
             customer_type=CustomerType.customer,
             counter_data=BillCounterData(
                 x_care_id=str(payment.location.external_id),
                 cashier_id=str(payment.created_by.external_id),
                 counter_name=payment.location.name,
             ),
         ).model_dump()
 
         logger.info("Odoo Payment Data: %s", data)
 
-        response = OdooConnector.call_api("api/account/move/payment", data)
-        return response["payment"]["id"]
+        try:
+            response = OdooConnector.call_api("api/account/move/payment", data)
+            return response.get("payment", {}).get("id")
+        except Exception as e:
+            logger.exception("Failed to sync payment %s to Odoo: %s", payment_id, e)
+            return None
🤖 Prompt for AI Agents
In odoo/resource/account_move_payment/payment.py around lines 22 to 73, the code
assumes payment.location and payment.created_by are always present and directly
indexes response["payment"]["id"]; add null checks and safe fallbacks: when
building BillCounterData use conditional values (e.g.,
str(payment.location.external_id) if payment.location else "" and
str(payment.created_by.external_id) if payment.created_by else "") and guard
counter_name with payment.location.name if present; after calling
OdooConnector.call_api validate that response is a dict and contains "payment"
and "id" (return None or raise a descriptive error if keys are missing) instead
of unguarded direct indexing.

Comment on lines +75 to +95
def sync_payment_cancel_to_odoo_api(self, payment_id: str) -> int | None:
"""
Synchronize a cancelled Django payment reconciliation to Odoo using the custom addon API.

Args:
payment_id: External ID of the Django payment reconciliation

Returns:
Odoo payment ID if successful, None otherwise
"""
payment = PaymentReconciliation.objects.get(external_id=payment_id)

data = AccountPaymentCancelApiRequest(
x_care_id=str(payment.external_id),
reason=payment.status,
).model_dump()

logger.info("Odoo Payment Cancel Data: %s", data)

response = OdooConnector.call_api("api/account/move/payment/cancel", data)
return response["payment"]["id"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling and use safe dictionary access.

Similar issues as the previous method:

  1. Line 95: Direct dictionary access could raise KeyError.
  2. No error handling around the API call.

Apply this diff:

     def sync_payment_cancel_to_odoo_api(self, payment_id: str) -> int | None:
         """
         Synchronize a cancelled Django payment reconciliation to Odoo using the custom addon API.
 
         Args:
             payment_id: External ID of the Django payment reconciliation
 
         Returns:
             Odoo payment ID if successful, None otherwise
         """
         payment = PaymentReconciliation.objects.get(external_id=payment_id)
 
         data = AccountPaymentCancelApiRequest(
             x_care_id=str(payment.external_id),
             reason=payment.status,
         ).model_dump()
 
         logger.info("Odoo Payment Cancel Data: %s", data)
 
-        response = OdooConnector.call_api("api/account/move/payment/cancel", data)
-        return response["payment"]["id"]
+        try:
+            response = OdooConnector.call_api("api/account/move/payment/cancel", data)
+            return response.get("payment", {}).get("id")
+        except Exception as e:
+            logger.exception("Failed to cancel payment %s in Odoo: %s", payment_id, e)
+            return None
🤖 Prompt for AI Agents
In odoo/resource/account_move_payment/payment.py around lines 75 to 95, the
method sync_payment_cancel_to_odoo_api lacks error handling and uses direct dict
indexing which can raise KeyError; update it to wrap the DB lookup and the
OdooConnector.call_api call in try/except blocks (catch
PaymentReconciliation.DoesNotExist and generic Exception as appropriate), log
the exception details, and return None on error, and replace
response["payment"]["id"] with safe access using .get (e.g.,
response.get("payment", {}).get("id")) or validate the structure before
returning to avoid KeyError.

Comment on lines +32 to +44
def get_product_purchase_price(self, product):
"""Get purchase price from charge item definition price components"""
if not product.charge_item_definition:
return None

for item in product.charge_item_definition.price_components:
if (
item["monetary_component_type"]
== MonetaryComponentType.informational.value
and item["code"]["code"] == "purchase_price"
):
return item["amount"]
return None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add defensive checks for nested dictionary access.

Line 41 accesses item["code"]["code"] without verifying that the "code" key exists in the item dictionary, which could raise a KeyError if the data structure doesn't match expectations.

Consider adding a safety check:

     def get_product_purchase_price(self, product):
         """Get purchase price from charge item definition price components"""
         if not product.charge_item_definition:
             return None
 
         for item in product.charge_item_definition.price_components:
             if (
                 item["monetary_component_type"]
                 == MonetaryComponentType.informational.value
-                and item["code"]["code"] == "purchase_price"
+                and item.get("code", {}).get("code") == "purchase_price"
             ):
                 return item["amount"]
         return None
🤖 Prompt for AI Agents
In odoo/resource/account_move/delivery_order.py around lines 32 to 44, the loop
accesses item["code"]["code"] (and item["amount"]) without verifying the "code"
key or nested structure; add defensive checks to avoid KeyError by first
ensuring item is a dict, that "code" exists and is a dict (or has a string
"code"), and that "amount" exists before comparing values or returning; update
the conditional to safely access nested keys (e.g., use .get and type checks)
and only return item["amount"] when present and valid.

Comment on lines 43 to 84
def sync_product_to_odoo_api(
self, charge_item_definition, hsn: str = ""
) -> int | None:
"""
Synchronize a charge item definition to Odoo as a product.

Args:
charge_item_definition: ChargeItemDefinition instance

Returns:
Odoo product ID if successful, None otherwise
"""
base_price = self.get_charge_item_base_price(charge_item_definition)
mrp = self.get_charge_item_mrp(charge_item_definition)
purchase_price = self.get_charge_item_purchase_price(charge_item_definition)

taxes = []
for tax in self.get_taxes(charge_item_definition):
taxes.append(
TaxData(
tax_name=tax["code"]["display"],
tax_percentage=float(tax["factor"]),
)
)
data = ProductData(
product_name=f"CARE: {charge_item_definition.title}",
x_care_id=str(charge_item_definition.external_id),
mrp=float(base_price or "0"),
cost=float(purchase_price or mrp or "0"),
category=CategoryData(
category_name=charge_item_definition.category.title,
parent_x_care_id=str(charge_item_definition.category.parent.external_id)
if charge_item_definition.category.parent
else "",
x_care_id=str(charge_item_definition.category.external_id),
),
taxes=taxes,
hsn=hsn,
).model_dump()

response = OdooConnector.call_api("api/add/product", data)
return response.get("product", {}).get("id")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add null check for category and error handling.

Two concerns here:

  1. Line 73: charge_item_definition.category.title will raise AttributeError if category is None (it's a nullable ForeignKey per the model definition).
  2. The API call lacks error handling, so failures propagate.

Apply this diff:

     def sync_product_to_odoo_api(
         self, charge_item_definition, hsn: str = ""
     ) -> int | None:
         """
         Synchronize a charge item definition to Odoo as a product.
 
         Args:
             charge_item_definition: ChargeItemDefinition instance
 
         Returns:
             Odoo product ID if successful, None otherwise
         """
+        if not charge_item_definition.category:
+            logger.warning(
+                "Cannot sync charge item %s to Odoo: missing category",
+                charge_item_definition.external_id
+            )
+            return None
+
         base_price = self.get_charge_item_base_price(charge_item_definition)
         mrp = self.get_charge_item_mrp(charge_item_definition)
         purchase_price = self.get_charge_item_purchase_price(charge_item_definition)
 
         taxes = []
         for tax in self.get_taxes(charge_item_definition):
             taxes.append(
                 TaxData(
                     tax_name=tax["code"]["display"],
                     tax_percentage=float(tax["factor"]),
                 )
             )
         data = ProductData(
             product_name=f"CARE: {charge_item_definition.title}",
             x_care_id=str(charge_item_definition.external_id),
             mrp=float(base_price or "0"),
             cost=float(purchase_price or mrp or "0"),
             category=CategoryData(
                 category_name=charge_item_definition.category.title,
                 parent_x_care_id=str(charge_item_definition.category.parent.external_id)
                 if charge_item_definition.category.parent
                 else "",
                 x_care_id=str(charge_item_definition.category.external_id),
             ),
             taxes=taxes,
             hsn=hsn,
         ).model_dump()
 
-        response = OdooConnector.call_api("api/add/product", data)
-        return response.get("product", {}).get("id")
+        try:
+            response = OdooConnector.call_api("api/add/product", data)
+            return response.get("product", {}).get("id")
+        except Exception as e:
+            logger.exception(
+                "Failed to sync product %s to Odoo: %s",
+                charge_item_definition.external_id,
+                e
+            )
+            return None

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In odoo/resource/product_product/resource.py around lines 43–84, avoid
AttributeError by null-checking charge_item_definition.category before accessing
.title, .parent, or .external_id: when category is None set CategoryData fields
(category_name, parent_x_care_id, x_care_id) to empty strings (or sensible
defaults) instead of dereferencing; and add error handling around the
OdooConnector.call_api call (wrap it in try/except Exception as e), log the
exception and return None on failure, and validate the response contains a
product id before returning it so API failures don’t propagate.

Comment on lines +18 to +21
metadata = organization.metadata or {}
email = metadata.get("email", "")
phone = metadata.get("phone", "")
state = metadata.get("state", "kerala")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hard-coded default state value lacks context.

The hard-coded "kerala" default for state (Line 21) appears to be location-specific. If organizations can exist outside Kerala, this default could send incorrect data to Odoo. Consider making this configurable or requiring the state field in metadata.

🤖 Prompt for AI Agents
In odoo/resource/res_partner/resource.py around lines 18 to 21, the code uses a
hard-coded state default "kerala" which is location-specific; replace this by
reading a configurable default (e.g., from settings/config or environment) and
use that fallback, or validate and require that 'state' be present in
organization.metadata and raise/return an error if missing; update the codepaths
and tests to retrieve the configured default (or enforce the required metadata)
so organizations outside Kerala do not get incorrect state values.

Comment on lines +35 to +42
@receiver(post_save, sender=User)
def sync_user_to_odoo(sender, instance, created, **kwargs):
"""
Signal handler to sync user to Odoo when created or updated.
"""
odoo_user = OdooUserResource()
odoo_user.sync_user_to_odoo_api(instance)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add conditional check and async task processing.

The signal handler has a couple of concerns:

  1. It runs on every User save (create or update), but doesn't check the created flag. This means updating any user field triggers an Odoo sync, which could create duplicates or unnecessary API calls.
  2. The synchronous API call blocks the request thread and runs within the database transaction, meaning Odoo failures will roll back the user save.

Consider using a Celery task for async processing:

+from django.db import transaction
+
 @receiver(post_save, sender=User)
 def sync_user_to_odoo(sender, instance, created, **kwargs):
     """
     Signal handler to sync user to Odoo when created or updated.
     """
-    odoo_user = OdooUserResource()
-    odoo_user.sync_user_to_odoo_api(instance)
+    if created:
+        transaction.on_commit(lambda: sync_user_to_odoo_task.delay(str(instance.external_id)))

And define the task:

from celery import shared_task

@shared_task
def sync_user_to_odoo_task(user_id: str):
    try:
        from care.users.models import User
        user = User.objects.get(external_id=user_id)
        odoo_user = OdooUserResource()
        odoo_user.sync_user_to_odoo_api(user)
    except Exception as e:
        logger.exception("Failed to sync user %s to Odoo", user_id)
🧰 Tools
🪛 Ruff (0.14.3)

36-36: Unused function argument: sender

(ARG001)


36-36: Unused function argument: created

(ARG001)


36-36: Unused function argument: kwargs

(ARG001)

Comment on lines +44 to +54
@receiver(post_save, sender=Invoice)
def save_fields_before_update(sender, instance, raw, using, update_fields, **kwargs):
if instance.status in [
InvoiceStatusOptions.issued.value,
]:
odoo_integration = OdooInvoiceResource()
odoo_integration.sync_invoice_to_odoo_api(instance.external_id)
elif instance.status in INVOICE_CANCELLED_STATUS:
odoo_integration = OdooInvoiceResource()
odoo_integration.sync_invoice_return_to_odoo_api(instance.external_id)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move Odoo API call outside transaction boundary.

The invoice sync occurs within the database transaction. If the Odoo API fails or times out, it will roll back the invoice status change, which may not be the desired behavior.

Consider deferring the API call until after the transaction commits:

+from django.db import transaction
+
 @receiver(post_save, sender=Invoice)
 def save_fields_before_update(sender, instance, raw, using, update_fields, **kwargs):
     if instance.status in [
         InvoiceStatusOptions.issued.value,
     ]:
-        odoo_integration = OdooInvoiceResource()
-        odoo_integration.sync_invoice_to_odoo_api(instance.external_id)
+        invoice_id = instance.external_id
+        transaction.on_commit(lambda: sync_invoice_task.delay(str(invoice_id), "issued"))
     elif instance.status in INVOICE_CANCELLED_STATUS:
-        odoo_integration = OdooInvoiceResource()
-        odoo_integration.sync_invoice_return_to_odoo_api(instance.external_id)
+        invoice_id = instance.external_id
+        transaction.on_commit(lambda: sync_invoice_task.delay(str(invoice_id), "cancelled"))

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.3)

45-45: Unused function argument: sender

(ARG001)


45-45: Unused function argument: raw

(ARG001)


45-45: Unused function argument: using

(ARG001)


45-45: Unused function argument: update_fields

(ARG001)


45-45: Unused function argument: kwargs

(ARG001)

Comment on lines +56 to +70
@receiver(post_save, sender=PaymentReconciliation)
def sync_payment_to_odoo(sender, instance, created, **kwargs):
"""
Signal handler to sync payment reconciliation to Odoo when created.
"""
if instance.status == PaymentReconciliationStatusOptions.active.value:
odoo_payment = OdooPaymentResource()
odoo_payment.sync_payment_to_odoo_api(instance.external_id)
elif instance.status in [
PaymentReconciliationStatusOptions.cancelled.value,
PaymentReconciliationStatusOptions.entered_in_error.value,
]:
odoo_payment = OdooPaymentResource()
odoo_payment.sync_payment_cancel_to_odoo_api(instance.external_id)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Apply transaction.on_commit pattern for reliability.

Similar to other handlers, the synchronous Odoo API call runs within the transaction, creating a coupling between payment persistence and external API availability.

Consider the same async pattern suggested for other handlers.

🧰 Tools
🪛 Ruff (0.14.3)

57-57: Unused function argument: sender

(ARG001)


57-57: Unused function argument: created

(ARG001)


57-57: Unused function argument: kwargs

(ARG001)

🤖 Prompt for AI Agents
In odoo/signals.py around lines 56 to 70, the handler currently calls Odoo API
synchronously inside the DB transaction; change it to use Django's
transaction.on_commit so the external API call runs only after the DB commit
succeeds — for created/active and for cancelled/entered_in_error, register a
callback with transaction.on_commit that instantiates OdooPaymentResource and
invokes the appropriate sync method (or dispatches the existing async task used
elsewhere) passing instance.external_id; import transaction from django.db and
ensure the callback captures only the external_id (not the full model instance)
to avoid stale data/lifecycle issues.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (10)
odoo/resource/account_move/invoice.py (6)

110-139: Batch-load related resources to avoid the N+1 avalanche.

We’re still issuing individual ServiceRequest/TokenBooking/MedicationDispense lookups per charge item. On a busy invoice this is dozens of round-trips. Please prefetch by type and reuse in-memory dictionaries.


29-29: Raise something more specific than Exception.

Same story as before: callers can’t react intelligently when you throw a plain Exception. Give us a dedicated error type (or ValueError) that callers can catch.


31-48: Use safe lookups for code["code"].

Both helper methods still assume every price component has a {"code": {"code": ...}} structure. One malformed entry triggers a KeyError. Please switch to .get checks (and guard unit_price_components when it’s None).


84-101: Handle missing categories before building CategoryData.

charge_item_definition.category is optional; when it’s absent the ternary touches .title/.parent and crashes. Mirror the fallback used elsewhere (log/return default CategoryData) before accessing those attributes.


157-158: Defend against malformed API responses.

response["invoice"]["id"] blindly indexes the payload; wrap it in safe access (and raise/log a descriptive error when missing) so we don’t crash with a KeyError mid-sync.


179-181: Same for the return flow.

response["reverse_invoice"]["id"] has the identical KeyError problem—harden it the same way.

odoo/resource/product_product/resource.py (2)

14-14: Stop raising bare Exception.

We already asked for this: callers can’t distinguish a missing base-price component when you throw a generic Exception. Give us a dedicated error (or at least a ValueError) so the sync flow can react sanely.


68-78: Guard against missing categories before we dereference them.

charge_item_definition.category is nullable; hitting this code with an uncategorized item blows up on Line 73. Please short-circuit (log and return/skip) when the category is absent before touching .title or .parent.

odoo/resource/account_move/delivery_order.py (2)

37-43: Defensive access for purchase_price, please.

We’re still indexing item["code"]["code"] blindly. One malformed component and the sync dies with a KeyError. Use .get (and maybe an isinstance check) so we only return amounts when the nested structure is actually there.


148-149: Don’t assume the API payload survived intact.

response["invoice"]["id"] will KeyError the moment Odoo gives us a partial or error payload. Use safe access (and log/fail gracefully) instead of indexing directly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86717a0 and 58fb9cc.

📒 Files selected for processing (4)
  • odoo/resource/account_move/delivery_order.py (1 hunks)
  • odoo/resource/account_move/invoice.py (1 hunks)
  • odoo/resource/product_product/resource.py (1 hunks)
  • odoo/resource/product_product/spec.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursorrules)

**/*.py: Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance).
Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables).

Files:

  • odoo/resource/account_move/invoice.py
  • odoo/resource/product_product/spec.py
  • odoo/resource/account_move/delivery_order.py
  • odoo/resource/product_product/resource.py
🧬 Code graph analysis (4)
odoo/resource/account_move/invoice.py (12)
care/emr/models/charge_item.py (1)
  • ChargeItem (7-47)
care/emr/models/invoice.py (1)
  • Invoice (7-31)
care/emr/models/medication_dispense.py (1)
  • MedicationDispense (8-34)
care/emr/models/scheduling/booking.py (1)
  • TokenBooking (22-50)
care/emr/models/service_request.py (1)
  • ServiceRequest (7-46)
care/emr/resources/charge_item/spec.py (1)
  • ChargeItemResourceOptions (37-41)
care/emr/resources/common/monetary_component.py (1)
  • MonetaryComponentType (9-14)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/account_move/spec.py (4)
  • AccountMoveApiRequest (26-33)
  • AccountMoveReturnApiRequest (36-43)
  • BillType (21-23)
  • InvoiceItem (13-18)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/product_product/spec.py (1)
  • ProductData (19-27)
odoo/resource/res_partner/spec.py (2)
  • PartnerData (11-19)
  • PartnerType (6-8)
odoo/resource/product_product/spec.py (1)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/account_move/delivery_order.py (8)
care/emr/models/supply_delivery.py (2)
  • DeliveryOrder (29-48)
  • SupplyDelivery (7-26)
care/emr/resources/common/monetary_component.py (1)
  • MonetaryComponentType (9-14)
care/emr/resources/inventory/supply_delivery/spec.py (1)
  • SupplyDeliveryStatusOptions (18-22)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/account_move/spec.py (3)
  • AccountMoveApiRequest (26-33)
  • BillType (21-23)
  • InvoiceItem (13-18)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/product_product/spec.py (1)
  • ProductData (19-27)
odoo/resource/res_partner/spec.py (2)
  • PartnerData (11-19)
  • PartnerType (6-8)
odoo/resource/product_product/resource.py (6)
care/emr/models/charge_item_definition.py (1)
  • ChargeItemDefinition (6-24)
care/emr/models/product.py (1)
  • Product (6-17)
care/emr/resources/common/monetary_component.py (1)
  • MonetaryComponentType (9-14)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/product_product/spec.py (2)
  • ProductData (19-27)
  • TaxData (8-10)
🪛 Ruff (0.14.4)
odoo/resource/account_move/invoice.py

29-29: Create your own exception

(TRY002)


29-29: Avoid specifying long messages outside the exception class

(TRY003)

odoo/resource/product_product/resource.py

14-14: Create your own exception

(TRY002)


14-14: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test / Test

Comment on lines +61 to +70
supplier_metadata = delivery_order.supplier.metadata or {}
partner_data = PartnerData(
name=delivery_order.supplier.name,
x_care_id=str(delivery_order.supplier.external_id),
partner_type=PartnerType.company,
phone=supplier_metadata.get("phone", ""),
state=supplier_metadata.get("state", "kerala"),
email=supplier_metadata.get("email", ""),
agent=False,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle missing suppliers before we touch metadata.

supplier on DeliveryOrder is nullable; when it’s absent this block instantly raises. Bail out (log/return) when delivery_order.supplier is falsy before reading metadata.

🤖 Prompt for AI Agents
In odoo/resource/account_move/delivery_order.py around lines 61 to 70, the code
assumes delivery_order.supplier is always present and directly accesses
supplier.metadata, which raises when supplier is None; add an early guard that
checks if not delivery_order.supplier and then either log a clear message and
return/skip processing (or raise a specific exception depending on surrounding
flow) before touching supplier metadata, otherwise proceed to build PartnerData
as before.

Comment on lines +84 to +123
for supply_delivery in supply_deliveries:
if supply_delivery.supplied_item:
product = supply_delivery.supplied_item
base_price = self.get_product_base_price(product)
purchase_price = self.get_product_purchase_price(product)

# Get category data if charge item definition exists
if (
product.charge_item_definition
and product.charge_item_definition.category
):
category_data = CategoryData(
category_name=product.charge_item_definition.category.title,
parent_x_care_id=str(
product.charge_item_definition.category.parent.external_id
)
if product.charge_item_definition.category.parent
else "",
x_care_id=str(
product.charge_item_definition.category.external_id
),
)
else:
category_data = CategoryData(
category_name="Uncategorized",
parent_x_care_id="",
x_care_id="",
)

product_data = ProductData(
product_name=product.product_knowledge.name
if product.product_knowledge
else "Unknown Product",
x_care_id=str(product.external_id),
mrp=float(base_price or "0"),
cost=float(purchase_price or base_price or "0"),
category=category_data,
status=product.charge_item_definition.status,
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Skip products without charge-item definitions.

Inside the loop we happily do product.charge_item_definition.status even when the product has no definition, leading to an AttributeError. Short-circuit (continue) when the definition is missing before calculating prices or building ProductData.

🤖 Prompt for AI Agents
In odoo/resource/account_move/delivery_order.py around lines 84 to 123, the loop
builds ProductData using product.charge_item_definition.status even when
charge_item_definition is None which raises AttributeError; modify the loop to
check if product.charge_item_definition is present at the start and immediately
continue (skip) if missing, before computing base_price/purchase_price or
constructing CategoryData/ProductData, so only products with a
charge_item_definition proceed.

Comment on lines +84 to +101
product_data = ProductData(
product_name=charge_item.charge_item_definition.title,
x_care_id=str(charge_item.charge_item_definition.external_id),
mrp=float(base_price or "0"),
cost=float(purchase_price or base_price or "0"),
category=CategoryData(
category_name=charge_item.charge_item_definition.category.title,
parent_x_care_id=str(
charge_item.charge_item_definition.category.parent.external_id
)
if charge_item.charge_item_definition.category.parent
else "",
x_care_id=str(
charge_item.charge_item_definition.category.external_id
),
),
status=charge_item.charge_item_definition.status,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Map status into the enum we defined.

Just like the product sync, feeding the raw charge_item_definition.status into ProductData will explode if the value isn’t one of the Enum literals. Normalize (and default) before constructing the payload.

Comment on lines +70 to +72
mrp=float(base_price or "0"),
cost=float(purchase_price or mrp or "0"),
category=CategoryData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the actual MRP you just fetched.

You compute mrp = self.get_charge_item_mrp(...) and then promptly ignore it, writing the base price into the MRP slot. That mis-prices every product lacking a base/ MRP match. Cache the resolved mrp/cost values before building ProductData and feed the right ones in.

🤖 Prompt for AI Agents
In odoo/resource/product_product/resource.py around lines 70 to 72, you are
currently writing base_price into the mrp field instead of the computed mrp from
self.get_charge_item_mrp(...). Capture the resolved mrp in a local variable (as
you already compute it), compute cost = float(purchase_price or mrp or "0"), and
use mrp = float(mrp or base_price or "0") (or similar order you intend) and then
pass those cached float values into ProductData (mrp=..., cost=...). Ensure you
convert to float once and reuse those variables when constructing
CategoryData/ProductData so the correct MRP and derived cost are saved.

Comment on lines +81 to +82
status=charge_item_definition.status,
).model_dump()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Translate status into the expected enum.

ProductData.status wants a ProductStatus; handing it the raw charge_item_definition.status string will trigger a validation error as soon as the value drifts outside {"active","retired","draft"}. Map/normalize it (with a sensible fallback) before instantiating ProductData.

🤖 Prompt for AI Agents
In odoo/resource/product_product/resource.py around lines 81 to 82, the code
passes the raw string charge_item_definition.status into ProductData, but
ProductData.status expects a ProductStatus enum; convert/normalize the incoming
string to the corresponding ProductStatus value (e.g., map "active" ->
ProductStatus.active, "retired" -> ProductStatus.retired, "draft" ->
ProductStatus.draft), handle case variations and unknown values by falling back
to a safe default (e.g., ProductStatus.draft or ProductStatus.retired as per
domain rules), and pass the enum value into ProductData before calling
.model_dump().

Comment on lines +13 to +27
class ProductStatus(str, Enum):
active = "active"
retired = "retired"
draft = "draft"


class ProductData(BaseModel):
product_name: str
x_care_id: str
cost: float
mrp: float
category: CategoryData
taxes: list[TaxData] | None = None
hsn: str | None = None
status: ProductStatus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Loosen the ProductStatus contract before this explodes in production.

ChargeItemDefinition.status (and Product.status) are free-form strings in our EMR models; constraining ProductData.status to ProductStatus(active|retired|draft) guarantees a ValidationError the moment a record carries something like "inactive" or "entered_in_error". Either broaden the enum to cover every stored value or just accept a plain string for now. Otherwise the sync dies before reaching Odoo.

🤖 Prompt for AI Agents
In odoo/resource/product_product/spec.py around lines 13 to 27,
ProductData.status is currently typed as a restrictive ProductStatus enum which
will raise ValidationError for any status values outside {active, retired,
draft}; change the declaration to accept a plain string instead (i.e., type
status: str) so the model accepts all EMR free-form status values during sync,
or alternatively expand ProductStatus to include every stored status if strict
enumeration is required — adjust any tests or downstream code that relied on the
enum accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
odoo/resource/account_move/invoice.py (1)

170-203: N+1 query issue already identified.

This code segment issues individual queries inside the loop for ServiceRequest, TokenBooking, and MedicationDispense. The previous review already covered this performance concern in detail.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1f5b62 and fb6a4ef.

📒 Files selected for processing (2)
  • odoo/resource/account_move/invoice.py (1 hunks)
  • odoo/resource/account_move/spec.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursorrules)

**/*.py: Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance).
Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables).

Files:

  • odoo/resource/account_move/invoice.py
  • odoo/resource/account_move/spec.py
🧬 Code graph analysis (2)
odoo/resource/account_move/invoice.py (13)
care/emr/models/charge_item.py (1)
  • ChargeItem (7-47)
care/emr/models/invoice.py (1)
  • Invoice (7-31)
care/emr/models/medication_dispense.py (1)
  • MedicationDispense (8-34)
care/emr/models/scheduling/booking.py (1)
  • TokenBooking (22-50)
care/emr/models/service_request.py (1)
  • ServiceRequest (7-46)
care/emr/resources/charge_item/spec.py (1)
  • ChargeItemResourceOptions (37-41)
care/emr/resources/common/monetary_component.py (1)
  • MonetaryComponentType (9-14)
odoo/connector/connector.py (2)
  • OdooConnector (12-71)
  • call_api (14-71)
odoo/resource/account_move/spec.py (7)
  • AccountMoveApiRequest (45-52)
  • AccountMoveReturnApiRequest (55-62)
  • BillType (40-42)
  • DiscountGroup (9-11)
  • DiscountType (14-16)
  • InvoiceDiscounts (19-24)
  • InvoiceItem (31-37)
odoo/resource/product_category/spec.py (1)
  • CategoryData (4-7)
odoo/resource/product_product/spec.py (1)
  • ProductData (19-27)
odoo/resource/res_partner/spec.py (2)
  • PartnerData (11-19)
  • PartnerType (6-8)
care/facility/models/patient_icmr.py (1)
  • email (34-35)
odoo/resource/account_move/spec.py (2)
odoo/resource/product_product/spec.py (1)
  • ProductData (19-27)
odoo/resource/res_partner/spec.py (1)
  • PartnerData (11-19)
🪛 Ruff (0.14.5)
odoo/resource/account_move/invoice.py

32-32: Create your own exception

(TRY002)


32-32: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test / Test
🔇 Additional comments (3)
odoo/resource/account_move/invoice.py (2)

54-105: LGTM: Defensive programming done right.

The get_first_discount method properly uses .get() with safe defaults throughout, avoiding the KeyError pitfalls present in the other helper methods.


143-144: Verify MRP and cost fallback logic.

Line 143 uses base_price as a fallback for mrp, and line 144 uses base_price as a second fallback for cost. Ensure that treating the base price as both MRP and purchase cost is semantically correct for your business logic—typically these represent different pricing tiers.

odoo/resource/account_move/spec.py (1)

1-62: Well-structured Pydantic models.

The specifications are cleanly defined with proper typing, sensible defaults, and correct use of optional fields. The model hierarchy (discount models, invoice items, and request payloads) is logical and maintainable.

x_care_id=str(invoice.patient.external_id),
partner_type=PartnerType.person,
phone=invoice.patient.phone_number,
state=invoice.facility.state or "kerala",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded state fallback seems arbitrary.

Defaulting to "kerala" when invoice.facility.state is None appears to be an India-specific assumption that may not hold for all facilities. Consider using a more generic default or requiring facilities to have a state configured.

-            state=invoice.facility.state or "kerala",
+            state=invoice.facility.state or "unknown",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
state=invoice.facility.state or "kerala",
state=invoice.facility.state or "unknown",
🤖 Prompt for AI Agents
In odoo/resource/account_move/invoice.py around line 127, the code falls back to
the hardcoded string "kerala" when invoice.facility.state is falsy; remove this
India-specific default and instead use a neutral, deterministic approach —
either use a contextual fallback such as invoice.company_state or None, or
raise/validate earlier to require facility.state. Replace the hardcoded literal
with invoice.facility.state or invoice.company_state (or None) and add a
validation/constraint on the facility model or invoice create/update path to
surface missing state values rather than silently defaulting to "kerala".

Comment on lines +234 to +237
data = AccountMoveReturnApiRequest(
x_care_id=str(invoice.external_id),
reason=invoice.status,
).model_dump()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use cancelled_reason instead of status for the return reason.

Line 236 maps invoice.status to the return reason, but the Invoice model has a dedicated cancelled_reason field (line 16 in care/emr/models/invoice.py) that should be used here for clarity and correctness.

     data = AccountMoveReturnApiRequest(
         x_care_id=str(invoice.external_id),
-        reason=invoice.status,
+        reason=invoice.cancelled_reason or invoice.status,
     ).model_dump()
🤖 Prompt for AI Agents
In odoo/resource/account_move/invoice.py around lines 234 to 237, the return
payload uses invoice.status for the reason but the Invoice model exposes a
dedicated cancelled_reason field; change the mapping to use
invoice.cancelled_reason instead of invoice.status (convert to string or handle
None consistently with other callers), so x_care_id remains
str(invoice.external_id) and reason becomes invoice.cancelled_reason (or
str(invoice.cancelled_reason) if you need a string).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
odoo/signals.py (3)

30-37: Still lacks created check and runs synchronously within transaction.

The handler continues to trigger on every User save (create or update) and makes a blocking Odoo API call within the database transaction, which couples external API availability to user persistence.


39-49: Odoo API call still occurs within transaction boundary.

The invoice sync continues to run within the database transaction. If Odoo fails or times out, the invoice status change will roll back.


51-65: Transaction.on_commit pattern still not applied.

The handler remains coupled to the database transaction, making synchronous Odoo API calls that can block the request thread and roll back payment persistence on external failures.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb6a4ef and fd48015.

📒 Files selected for processing (1)
  • odoo/signals.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (.cursorrules)

**/*.py: Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance).
Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables).

Files:

  • odoo/signals.py
**/signals.py

📄 CodeRabbit inference engine (.cursorrules)

Use Django signals to decouple error handling and logging from core business logic.

Files:

  • odoo/signals.py
🧠 Learnings (1)
📚 Learning: 2025-07-18T15:17:40.483Z
Learnt from: CR
Repo: ohcnetwork/care PR: 0
File: .cursorrules:0-0
Timestamp: 2025-07-18T15:17:40.483Z
Learning: Applies to **/signals.py : Use Django signals to decouple error handling and logging from core business logic.

Applied to files:

  • odoo/signals.py
🧬 Code graph analysis (1)
odoo/signals.py (13)
care/emr/models/charge_item_definition.py (1)
  • ChargeItemDefinition (6-24)
care/emr/models/invoice.py (1)
  • Invoice (7-31)
care/emr/models/organization.py (1)
  • Organization (106-107)
care/emr/models/payment_reconciliation.py (1)
  • PaymentReconciliation (6-29)
care/emr/models/product.py (1)
  • Product (6-17)
care/emr/models/resource_category.py (1)
  • ResourceCategory (10-86)
care/users/models.py (1)
  • User (196-436)
odoo/resource/account_move/invoice.py (3)
  • OdooInvoiceResource (27-241)
  • sync_invoice_to_odoo_api (107-218)
  • sync_invoice_return_to_odoo_api (220-241)
odoo/resource/account_move_payment/payment.py (3)
  • OdooPaymentResource (21-95)
  • sync_payment_to_odoo_api (22-73)
  • sync_payment_cancel_to_odoo_api (75-95)
odoo/resource/product_category/category.py (2)
  • OdooCategoryResource (5-25)
  • sync_category_to_odoo_api (6-25)
odoo/resource/product_product/resource.py (3)
  • OdooProductProductResource (9-107)
  • sync_product_to_odoo_api (43-85)
  • sync_product_from_product_model (87-107)
odoo/resource/res_partner/resource.py (2)
  • OdooPartnerResource (6-34)
  • sync_partner_to_odoo_api (7-34)
odoo/resource/res_user/resource.py (2)
  • OdooUserResource (7-46)
  • sync_user_to_odoo_api (13-46)
🪛 Ruff (0.14.5)
odoo/signals.py

31-31: Unused function argument: sender

(ARG001)


31-31: Unused function argument: created

(ARG001)


31-31: Unused function argument: kwargs

(ARG001)


40-40: Unused function argument: sender

(ARG001)


40-40: Unused function argument: raw

(ARG001)


40-40: Unused function argument: using

(ARG001)


40-40: Unused function argument: update_fields

(ARG001)


40-40: Unused function argument: kwargs

(ARG001)


52-52: Unused function argument: sender

(ARG001)


52-52: Unused function argument: created

(ARG001)


52-52: Unused function argument: kwargs

(ARG001)


68-68: Unused function argument: sender

(ARG001)


68-68: Unused function argument: created

(ARG001)


68-68: Unused function argument: kwargs

(ARG001)


77-77: Unused function argument: sender

(ARG001)


77-77: Unused function argument: created

(ARG001)


77-77: Unused function argument: kwargs

(ARG001)


90-90: Unused function argument: sender

(ARG001)


90-90: Unused function argument: created

(ARG001)


90-90: Unused function argument: kwargs

(ARG001)


113-113: Unused function argument: sender

(ARG001)


113-113: Unused function argument: created

(ARG001)


113-113: Unused function argument: kwargs

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test / Test

Comment on lines +67 to +74
@receiver(post_save, sender=ChargeItemDefinition)
def sync_charge_item_definition_to_odoo(sender, instance, created, **kwargs):
"""
Signal handler to sync charge item definition to Odoo as a product when created or updated.
"""
odoo_product = OdooProductProductResource()
odoo_product.sync_product_to_odoo_api(instance)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Synchronous Odoo sync on every ChargeItemDefinition save without guards.

This handler fires on every save (create and update) without checking the created flag, and the synchronous API call runs within the database transaction, blocking the request thread and causing rollbacks if Odoo is unavailable.

Consider deferring the sync with transaction.on_commit and checking the created flag:

+from django.db import transaction
+
 @receiver(post_save, sender=ChargeItemDefinition)
 def sync_charge_item_definition_to_odoo(sender, instance, created, **kwargs):
     """
     Signal handler to sync charge item definition to Odoo as a product when created or updated.
     """
-    odoo_product = OdooProductProductResource()
-    odoo_product.sync_product_to_odoo_api(instance)
+    if created:
+        charge_item_id = str(instance.external_id)
+        transaction.on_commit(lambda: sync_charge_item_to_odoo_task(charge_item_id))

And implement an async task (Celery or similar) to perform the actual sync outside the transaction boundary.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.5)

68-68: Unused function argument: sender

(ARG001)


68-68: Unused function argument: created

(ARG001)


68-68: Unused function argument: kwargs

(ARG001)

🤖 Prompt for AI Agents
In odoo/signals.py around lines 67 to 74, the current post_save handler
synchronously calls OdooProductProductResource.sync_product_to_odoo_api on every
save (create and update) inside the DB transaction; change it to only trigger on
creation (check the created flag) and defer the actual sync until after the
transaction commits via transaction.on_commit, and instead of making the API
call inline schedule an asynchronous background task (e.g., Celery, RQ or Django
background task) that accepts the instance PK and performs the sync outside the
request/transaction, with proper error handling and logging in the task so Odoo
downtime does not cause request blocking or rollbacks.

Comment on lines +76 to +87
@receiver(post_save, sender=ResourceCategory)
def sync_resource_category_to_odoo(sender, instance, created, **kwargs):
"""
Signal handler to sync resource category to Odoo when created or updated.
"""
if (
instance.resource_type
== ResourceCategoryResourceTypeOptions.charge_item_definition.value
):
odoo_category = OdooCategoryResource()
odoo_category.sync_category_to_odoo_api(instance)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Synchronous API call within transaction for category sync.

While the resource_type check is sensible, the handler still makes a blocking Odoo API call within the database transaction on every save, which can roll back the category save if Odoo is unavailable.

Apply transaction.on_commit to decouple the external API call:

+from django.db import transaction
+
 @receiver(post_save, sender=ResourceCategory)
 def sync_resource_category_to_odoo(sender, instance, created, **kwargs):
     """
     Signal handler to sync resource category to Odoo when created or updated.
     """
     if (
         instance.resource_type
         == ResourceCategoryResourceTypeOptions.charge_item_definition.value
     ):
-        odoo_category = OdooCategoryResource()
-        odoo_category.sync_category_to_odoo_api(instance)
+        category_id = str(instance.external_id)
+        transaction.on_commit(lambda: sync_category_to_odoo_task(category_id))

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.5)

77-77: Unused function argument: sender

(ARG001)


77-77: Unused function argument: created

(ARG001)


77-77: Unused function argument: kwargs

(ARG001)

🤖 Prompt for AI Agents
In odoo/signals.py around lines 76 to 87, the signal handler is making a
synchronous Odoo API call inside the DB transaction; move that external call
into a transaction.on_commit callback so the API call runs only after the save
successfully commits. Capture the instance.pk (or serialize the minimal data
needed) before registering the callback, and inside the on_commit callback
re-fetch the instance or reconstruct the payload and call
OdooCategoryResource().sync_category_to_odoo_api(...) so the external request
does not block or cause transactions to roll back if Odoo is unavailable.

Comment on lines +89 to +97
@receiver(post_save, sender=Organization)
def sync_organization_to_odoo(sender, instance, created, **kwargs):
"""
Signal handler to sync organization to Odoo as a partner when org_type is product_supplier.
"""
if instance.org_type == OrganizationTypeChoices.product_supplier.value:
odoo_partner = OdooPartnerResource()
odoo_partner.sync_partner_to_odoo_api(instance)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Partner sync runs synchronously within the transaction.

The org_type check is appropriate, but the handler makes a blocking Odoo API call within the database transaction, which can cause the organization save to roll back if Odoo is unreachable.

Defer the API call with transaction.on_commit:

+from django.db import transaction
+
 @receiver(post_save, sender=Organization)
 def sync_organization_to_odoo(sender, instance, created, **kwargs):
     """
     Signal handler to sync organization to Odoo as a partner when org_type is product_supplier.
     """
     if instance.org_type == OrganizationTypeChoices.product_supplier.value:
-        odoo_partner = OdooPartnerResource()
-        odoo_partner.sync_partner_to_odoo_api(instance)
+        org_id = str(instance.external_id)
+        transaction.on_commit(lambda: sync_organization_to_odoo_task(org_id))

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.5)

90-90: Unused function argument: sender

(ARG001)


90-90: Unused function argument: created

(ARG001)


90-90: Unused function argument: kwargs

(ARG001)

🤖 Prompt for AI Agents
In odoo/signals.py around lines 89 to 97, the signal handler currently makes a
blocking Odoo API call inside the DB transaction; change it to defer the sync
using transaction.on_commit so the API call runs only after the save commits.
Import django.db.transaction, capture any minimal data needed (e.g., instance.pk
or serialized fields) and inside transaction.on_commit schedule a callable
(lambda or function) that creates OdooPartnerResource and calls
sync_partner_to_odoo_api using the captured data; this avoids holding the
transaction open and prevents the save from rolling back if Odoo is unreachable.

Comment on lines +112 to +118
@receiver(post_save, sender=Product)
def sync_product_to_odoo(sender, instance, created, **kwargs):
"""
Signal handler to sync product to Odoo when it has a charge item definition.
"""
odoo_product = OdooProductProductResource()
odoo_product.sync_product_from_product_model(instance)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Product sync fires on every save without transaction safety.

This handler triggers on every Product save without checking the created flag, and makes a synchronous API call within the database transaction, coupling product persistence to Odoo availability.

Use transaction.on_commit and check the created flag:

+from django.db import transaction
+
 @receiver(post_save, sender=Product)
 def sync_product_to_odoo(sender, instance, created, **kwargs):
     """
     Signal handler to sync product to Odoo when it has a charge item definition.
     """
-    odoo_product = OdooProductProductResource()
-    odoo_product.sync_product_from_product_model(instance)
+    if created:
+        product_id = str(instance.external_id)
+        transaction.on_commit(lambda: sync_product_to_odoo_task(product_id))

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.5)

113-113: Unused function argument: sender

(ARG001)


113-113: Unused function argument: created

(ARG001)


113-113: Unused function argument: kwargs

(ARG001)

🤖 Prompt for AI Agents
In odoo/signals.py around lines 112 to 118, the post_save handler currently runs
on every save and calls Odoo synchronously inside the DB transaction; change it
to first check the created flag and whether the Product has a charge item
definition, then schedule the sync call inside transaction.on_commit so it runs
after commit (and preferably dispatch to a background task/worker rather than
performing a blocking API call in-process). Ensure the on_commit callback
invokes OdooProductProductResource().sync_product_from_product_model(instance)
(or a Celery/RQ task wrapper) so the sync is executed only after successful
commit and not during the DB transaction.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (8)
odoo/signals.py (8)

35-41: User sync runs synchronously on every save, coupled to DB transaction.

Right now every User.save() (create or any update) triggers a blocking Odoo call, and likely inside the DB transaction. That means:

  • Any Odoo timeout/error can roll back user creation/update.
  • All innocuous updates (profile tweaks, last_login changes, etc.) still hit Odoo, which is probably not what you want with an api/add/user endpoint.

At minimum I’d suggest:

  • Run the sync in a transaction.on_commit callback so DB state is final before you touch Odoo.
  • Decide whether you truly want this on every save vs. only on creation or on specific field changes.

For example, a minimal on-commit pattern:

+from django.db import transaction
+
 @receiver(post_save, sender=User)
 def sync_user_to_odoo(sender, instance, created, **kwargs):
-    """
-    Signal handler to sync user to Odoo when created or updated.
-    """
-    odoo_user = OdooUserResource()
-    odoo_user.sync_user_to_odoo_api(instance)
+    """
+    Signal handler to sync user to Odoo after commit.
+    """
+    user_external_id = str(instance.external_id)
+
+    def _sync():
+        user = User.objects.get(external_id=user_external_id)
+        odoo_user = OdooUserResource()
+        odoo_user.sync_user_to_odoo_api(user)
+
+    transaction.on_commit(_sync)

If you already have Celery/RQ in this project, pushing _sync into a background task from inside on_commit would avoid blocking the request thread altogether.


44-53: Invoice sync/cancel still happens inline in the transaction.

save_fields_before_update directly calls OdooInvoiceResource inside the handler whenever status is issued or in INVOICE_CANCELLED_STATUS. Given invoice creation/updates generally run inside atomic blocks, an Odoo timeout here can roll back billing changes and hold open DB transactions longer than necessary.

To reduce coupling:

+from django.db import transaction
+
 @receiver(post_save, sender=Invoice)
 def save_fields_before_update(sender, instance, raw, using, update_fields, **kwargs):
-    if instance.status in [
-        InvoiceStatusOptions.issued.value,
-    ]:
-        odoo_integration = OdooInvoiceResource()
-        odoo_integration.sync_invoice_to_odoo_api(instance.external_id)
-    elif instance.status in INVOICE_CANCELLED_STATUS:
-        odoo_integration = OdooInvoiceResource()
-        odoo_integration.sync_invoice_return_to_odoo_api(instance.external_id)
+    invoice_id = str(instance.external_id)
+
+    if instance.status == InvoiceStatusOptions.issued.value:
+        def _sync_issued():
+            odoo_integration = OdooInvoiceResource()
+            odoo_integration.sync_invoice_to_odoo_api(invoice_id)
+        transaction.on_commit(_sync_issued)
+    elif instance.status in INVOICE_CANCELLED_STATUS:
+        def _sync_cancel():
+            odoo_integration = OdooInvoiceResource()
+            odoo_integration.sync_invoice_return_to_odoo_api(invoice_id)
+        transaction.on_commit(_sync_cancel)

You can swap _sync_* for async tasks if you don’t want invoice posting to block the request thread.


56-69: Payment reconciliation sync/cancel also runs synchronously inside the save path.

The status-based branching looks fine, but both active and cancel flows still call Odoo directly from the signal. That means payment posting or cancellation is effectively “all or nothing” with Odoo, and any remote error bubbles back into the DB transaction.

Pattern wise, this should follow the same transaction.on_commit approach as invoices:

+from django.db import transaction
+
 @receiver(post_save, sender=PaymentReconciliation)
 def sync_payment_to_odoo(sender, instance, created, **kwargs):
@@
-    if instance.status == PaymentReconciliationStatusOptions.active.value:
-        odoo_payment = OdooPaymentResource()
-        odoo_payment.sync_payment_to_odoo_api(instance.external_id)
-    elif instance.status in [
-        PaymentReconciliationStatusOptions.cancelled.value,
-        PaymentReconciliationStatusOptions.entered_in_error.value,
-    ]:
-        odoo_payment = OdooPaymentResource()
-        odoo_payment.sync_payment_cancel_to_odoo_api(instance.external_id)
+    payment_id = str(instance.external_id)
+
+    if instance.status == PaymentReconciliationStatusOptions.active.value:
+        def _sync_active():
+            odoo_payment = OdooPaymentResource()
+            odoo_payment.sync_payment_to_odoo_api(payment_id)
+        transaction.on_commit(_sync_active)
+    elif instance.status in [
+        PaymentReconciliationStatusOptions.cancelled.value,
+        PaymentReconciliationStatusOptions.entered_in_error.value,
+    ]:
+        def _sync_cancel():
+            odoo_payment = OdooPaymentResource()
+            odoo_payment.sync_payment_cancel_to_odoo_api(payment_id)
+        transaction.on_commit(_sync_cancel)

This keeps “when to sync” semantics unchanged but moves “how” out of the transaction.


72-78: ChargeItemDefinition sync fires on every save and blocks on Odoo.

Every save of a ChargeItemDefinition goes straight through to OdooProductProductResource.sync_product_to_odoo_api synchronously. Depending on how often these are edited and how api/add/product behaves, this can be:

  • Lots of redundant Odoo calls, and
  • A source of failed saves if Odoo has issues.

At least moving the call to transaction.on_commit would de-risk persistence; you might also decide to only sync on initial creation or on particular status transitions if the Odoo endpoint is not idempotent.

Example:

+from django.db import transaction
+
 @receiver(post_save, sender=ChargeItemDefinition)
 def sync_charge_item_definition_to_odoo(sender, instance, created, **kwargs):
@@
-    odoo_product = OdooProductProductResource()
-    odoo_product.sync_product_to_odoo_api(instance)
+    def _sync():
+        odoo_product = OdooProductProductResource()
+        odoo_product.sync_product_to_odoo_api(instance)
+
+    transaction.on_commit(_sync)

You can later tighten the trigger condition (e.g. if created or based on instance.status) once you’ve confirmed the desired behaviour with the Odoo side.


81-91: ResourceCategory sync does external I/O in the signal despite type guard.

The resource_type check is good, but once it passes you still call Odoo synchronously and, again, likely while the DB transaction is open. That’s the same failure/coupling pattern as the other handlers.

A small restructuring would align it with the recommended pattern:

+from django.db import transaction
+
 @receiver(post_save, sender=ResourceCategory)
 def sync_resource_category_to_odoo(sender, instance, created, **kwargs):
@@
-    if (
-        instance.resource_type
-        == ResourceCategoryResourceTypeOptions.charge_item_definition.value
-    ):
-        odoo_category = OdooCategoryResource()
-        odoo_category.sync_category_to_odoo_api(instance)
+    if (
+        instance.resource_type
+        == ResourceCategoryResourceTypeOptions.charge_item_definition.value
+    ):
+        category_id = str(instance.external_id)
+
+        def _sync():
+            category = ResourceCategory.objects.get(external_id=category_id)
+            odoo_category = OdooCategoryResource()
+            odoo_category.sync_category_to_odoo_api(category)
+
+        transaction.on_commit(_sync)

94-101: Organization→partner sync is still directly bound to Odoo availability.

For Organization with org_type == product_supplier, the signal immediately calls Odoo. If Odoo is down, that will cause supplier save/update to fail, which is usually not what you want for core master data.

Same story: push the external call into an on_commit callback (and optionally into a background worker):

+from django.db import transaction
+
 @receiver(post_save, sender=Organization)
 def sync_organization_to_odoo(sender, instance, created, **kwargs):
@@
-    if instance.org_type == OrganizationTypeChoices.product_supplier.value:
-        odoo_partner = OdooPartnerResource()
-        odoo_partner.sync_partner_to_odoo_api(instance)
+    if instance.org_type == OrganizationTypeChoices.product_supplier.value:
+        org_id = str(instance.external_id)
+
+        def _sync():
+            org = Organization.objects.get(external_id=org_id)
+            odoo_partner = OdooPartnerResource()
+            odoo_partner.sync_partner_to_odoo_api(org)
+
+        transaction.on_commit(_sync)

104-114: DeliveryOrder→vendor bill sync is a heavy call inside the signal.

For completed delivery orders without an origin, you post a vendor bill to Odoo synchronously on every qualifying save. Given the amount of data involved and external latency, this is a prime candidate for transaction.on_commit + async.

A minimal on-commit refactor:

+from django.db import transaction
+
 @receiver(post_save, sender=DeliveryOrder)
 def sync_delivery_order_to_odoo(sender, instance, created, **kwargs):
@@
-    if (
-        instance.status == SupplyDeliveryOrderStatusOptions.completed.value
-        and not instance.origin
-    ):
-        odoo_delivery_order = OdooDeliveryOrderResource()
-        odoo_delivery_order.sync_delivery_order_to_odoo_api(instance.external_id)
+    if (
+        instance.status == SupplyDeliveryOrderStatusOptions.completed.value
+        and not instance.origin
+    ):
+        delivery_order_id = str(instance.external_id)
+
+        def _sync():
+            odoo_delivery_order = OdooDeliveryOrderResource()
+            odoo_delivery_order.sync_delivery_order_to_odoo_api(delivery_order_id)
+
+        transaction.on_commit(_sync)

If you already have a worker infrastructure, wrapping _sync in a task would be an easy next step.


117-123: Product sync is synchronous and unconditional; rely on on_commit and existing guard.

sync_product_to_odoo currently calls sync_product_from_product_model directly for every Product save. The actual guard on charge_item_definition lives inside OdooProductProductResource.sync_product_from_product_model, which is fine, but this is still a synchronous external call in the signal.

You can keep the existing semantics and just move it out of the transaction:

+from django.db import transaction
+
 @receiver(post_save, sender=Product)
 def sync_product_to_odoo(sender, instance, created, **kwargs):
@@
-    odoo_product = OdooProductProductResource()
-    odoo_product.sync_product_from_product_model(instance)
+    product_id = instance.pk
+
+    def _sync():
+        product = Product.objects.get(pk=product_id)
+        odoo_product = OdooProductProductResource()
+        odoo_product.sync_product_from_product_model(product)
+
+    transaction.on_commit(_sync)

That keeps the “only if it has a charge_item_definition” logic centralised in the resource class, but no longer risks rolling back a Product save due to Odoo.

🧹 Nitpick comments (1)
odoo/signals.py (1)

35-123: Ruff ARG001 warnings for unused signal arguments.

Ruff is flagging all the unused sender, created, raw, using, update_fields, and kwargs parameters. Django’s signal API does pass these (by name), so you can’t safely rename them to _sender / _created without changing the call signature, but you also don’t have to pretend to use them everywhere either.

Two low-friction options:

  • Add # noqa: ARG001 on the function definitions where you intentionally ignore the extra args, e.g.:
@receiver(post_save, sender=User)
def sync_user_to_odoo(sender, instance, created, **kwargs):  # noqa: ARG001
    ...
  • Or, if you really want to avoid noqa, explicitly consume them:
def sync_user_to_odoo(sender, instance, created, **kwargs):
    _ = (sender, created, kwargs)
    ...

I’d lean toward a noqa here so the intent stays clear and the signature matches Django’s docs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd48015 and 117f738.

📒 Files selected for processing (1)
  • odoo/signals.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (.cursorrules)

**/*.py: Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance).
Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables).

Files:

  • odoo/signals.py
**/signals.py

📄 CodeRabbit inference engine (.cursorrules)

Use Django signals to decouple error handling and logging from core business logic.

Files:

  • odoo/signals.py
🧠 Learnings (1)
📚 Learning: 2025-07-18T15:17:40.483Z
Learnt from: CR
Repo: ohcnetwork/care PR: 0
File: .cursorrules:0-0
Timestamp: 2025-07-18T15:17:40.483Z
Learning: Applies to **/signals.py : Use Django signals to decouple error handling and logging from core business logic.

Applied to files:

  • odoo/signals.py
🧬 Code graph analysis (1)
odoo/signals.py (19)
care/emr/models/charge_item_definition.py (1)
  • ChargeItemDefinition (6-24)
care/emr/models/invoice.py (1)
  • Invoice (7-31)
care/emr/models/payment_reconciliation.py (1)
  • PaymentReconciliation (6-29)
care/emr/models/product.py (1)
  • Product (6-17)
care/emr/models/resource_category.py (1)
  • ResourceCategory (10-86)
care/emr/models/supply_delivery.py (1)
  • DeliveryOrder (29-48)
care/emr/resources/inventory/supply_delivery/delivery_order.py (1)
  • SupplyDeliveryOrderStatusOptions (18-24)
care/emr/resources/invoice/spec.py (1)
  • InvoiceStatusOptions (15-20)
care/emr/resources/organization/spec.py (1)
  • OrganizationTypeChoices (11-15)
care/emr/resources/payment_reconciliation/spec.py (1)
  • PaymentReconciliationStatusOptions (22-26)
care/emr/resources/resource_category/spec.py (1)
  • ResourceCategoryResourceTypeOptions (10-13)
care/users/models.py (1)
  • User (196-436)
odoo/resource/account_move/delivery_order.py (2)
  • OdooDeliveryOrderResource (21-149)
  • sync_delivery_order_to_odoo_api (46-149)
odoo/resource/account_move/invoice.py (3)
  • OdooInvoiceResource (27-241)
  • sync_invoice_to_odoo_api (107-218)
  • sync_invoice_return_to_odoo_api (220-241)
odoo/resource/account_move_payment/payment.py (3)
  • OdooPaymentResource (21-95)
  • sync_payment_to_odoo_api (22-73)
  • sync_payment_cancel_to_odoo_api (75-95)
odoo/resource/product_category/category.py (2)
  • OdooCategoryResource (5-25)
  • sync_category_to_odoo_api (6-25)
odoo/resource/product_product/resource.py (3)
  • OdooProductProductResource (9-107)
  • sync_product_to_odoo_api (43-85)
  • sync_product_from_product_model (87-107)
odoo/resource/res_partner/resource.py (2)
  • OdooPartnerResource (6-34)
  • sync_partner_to_odoo_api (7-34)
odoo/resource/res_user/resource.py (2)
  • OdooUserResource (7-46)
  • sync_user_to_odoo_api (13-46)
🪛 Ruff (0.14.5)
odoo/signals.py

36-36: Unused function argument: sender

(ARG001)


36-36: Unused function argument: created

(ARG001)


36-36: Unused function argument: kwargs

(ARG001)


45-45: Unused function argument: sender

(ARG001)


45-45: Unused function argument: raw

(ARG001)


45-45: Unused function argument: using

(ARG001)


45-45: Unused function argument: update_fields

(ARG001)


45-45: Unused function argument: kwargs

(ARG001)


57-57: Unused function argument: sender

(ARG001)


57-57: Unused function argument: created

(ARG001)


57-57: Unused function argument: kwargs

(ARG001)


73-73: Unused function argument: sender

(ARG001)


73-73: Unused function argument: created

(ARG001)


73-73: Unused function argument: kwargs

(ARG001)


82-82: Unused function argument: sender

(ARG001)


82-82: Unused function argument: created

(ARG001)


82-82: Unused function argument: kwargs

(ARG001)


95-95: Unused function argument: sender

(ARG001)


95-95: Unused function argument: created

(ARG001)


95-95: Unused function argument: kwargs

(ARG001)


105-105: Unused function argument: sender

(ARG001)


105-105: Unused function argument: created

(ARG001)


105-105: Unused function argument: kwargs

(ARG001)


118-118: Unused function argument: sender

(ARG001)


118-118: Unused function argument: created

(ARG001)


118-118: Unused function argument: kwargs

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test / Test
🔇 Additional comments (1)
odoo/signals.py (1)

1-32: Confirmed: all handlers sync to Odoo inline without transaction.on_commit().

The analysis is spot-on. All 8 signal handlers execute synchronous Odoo API calls directly—no transaction deferral pattern anywhere. Each handler (lines 36–118) instantiates an Odoo resource class and calls its sync method immediately, without wrapping in transaction.on_commit(). The transaction module isn't imported, and there's zero use of on_commit in the file.

This tight coupling between external API and database transaction is a real architectural risk: if Odoo is unavailable or slow, you're rolling back core EMR persistence or stalling request threads. The recommendation to import transaction and consistently defer these calls until after commit is a sound mitigation and aligns with Django best practices.

⛔ Skipped due to learnings
Learnt from: CR
Repo: ohcnetwork/care PR: 0
File: .cursorrules:0-0
Timestamp: 2025-07-18T15:17:40.483Z
Learning: Applies to **/signals.py : Use Django signals to decouple error handling and logging from core business logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants