From f846f5a9e498d414fa3c1319d10403a473d3180b Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Wed, 6 Nov 2024 10:55:38 +0530 Subject: [PATCH 01/13] added the exchange rate table --- .../migrations/0061_exchangerate.py | 46 +++++++++++++++++++ commcare_connect/opportunity/models.py | 7 +++ commcare_connect/opportunity/visit_import.py | 27 ++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 commcare_connect/opportunity/migrations/0061_exchangerate.py diff --git a/commcare_connect/opportunity/migrations/0061_exchangerate.py b/commcare_connect/opportunity/migrations/0061_exchangerate.py new file mode 100644 index 00000000..6199d473 --- /dev/null +++ b/commcare_connect/opportunity/migrations/0061_exchangerate.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.5 on 2024-11-06 05:16 + +from django.db import migrations, models +from django.db.models.functions import TruncDate + +from commcare_connect.opportunity.visit_import import get_exchange_rate + + +def update_exchange_rate(apps, schema_editor): + Payment = apps.get_model("opportunity.Payment") + payments = ( + Payment.objects.annotate(date_only=TruncDate("date_paid")) + .values("date_only", "opportunity_access__opportunity__currency") + .distinct() + ) + + for payment in payments: + date_paid = payment["date_only"] + currency = payment["opportunity_access__opportunity__currency"] + + if currency in ["USD", None]: + exchange_rate = 1 + else: + exchange_rate = get_exchange_rate(currency, date_paid) + if not exchange_rate: + raise Exception(f"Invalid currency code {currency}") + + +class Migration(migrations.Migration): + dependencies = [ + ("opportunity", "0060_completedwork_payment_date"), + ] + + operations = [ + migrations.CreateModel( + name="ExchangeRate", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("currency_code", models.CharField(max_length=3)), + ("rate", models.DecimalField(decimal_places=6, max_digits=10)), + ("rate_date", models.DateField()), + ("fetched_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.RunPython(update_exchange_rate, migrations.RunPython.noop), + ] diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index 17f68978..4d2f5c04 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -685,3 +685,10 @@ class CatchmentArea(models.Model): class Meta: unique_together = ("site_code", "opportunity") + + +class ExchangeRate(models.Model): + currency_code = models.CharField(max_length=3) + rate = models.DecimalField(max_digits=10, decimal_places=6) + rate_date = models.DateField() + fetched_at = models.DateTimeField(auto_now_add=True) diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index e57cbd57..e4a0d8cd 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -16,6 +16,7 @@ CatchmentArea, CompletedWork, CompletedWorkStatus, + ExchangeRate, Opportunity, OpportunityAccess, Payment, @@ -290,6 +291,10 @@ def get_exchange_rate(currency_code, date=None): if currency_code == "USD": return 1 + rate = get_exchange_rate_from_db(currency_code, date) + if rate: + return rate + base_url = "https://openexchangerates.org/api" if date: url = f"{base_url}/historical/{date.strftime('%Y-%m-%d')}.json" @@ -297,7 +302,27 @@ def get_exchange_rate(currency_code, date=None): url = f"{base_url}/latest.json" url = f"{url}?app_id={settings.OPEN_EXCHANGE_RATES_API_ID}" rates = json.load(urllib.request.urlopen(url)) - return rates["rates"].get(currency_code) + + rate = rates["rates"].get(currency_code) + + rate_date = date if date else now().date() + ExchangeRate.objects.create(currency_code=currency_code, rate=rate, rate_date=rate_date) + + return rate + + +def get_exchange_rate_from_db(currency_code, date=None): + if currency_code == "USD": + return 1 + + if not date: + date = now().date() + + rate = ( + ExchangeRate.objects.filter(currency_code=currency_code, rate_date=date).values_list("rate", flat=True).first() + ) + + return rate def bulk_update_completed_work_status(opportunity: Opportunity, file: UploadedFile) -> CompletedWorkImportStatus: From c70fa93a7ad2db25ee3ea1ccfcb9a3730c34ff68 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Wed, 6 Nov 2024 13:04:58 +0530 Subject: [PATCH 02/13] removed cache --- commcare_connect/opportunity/visit_import.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index e4a0d8cd..f7976d06 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -11,7 +11,6 @@ from django.utils.timezone import now from tablib import Dataset -from commcare_connect.cache import quickcache from commcare_connect.opportunity.models import ( CatchmentArea, CompletedWork, @@ -278,11 +277,6 @@ def _bulk_update_payments(opportunity: Opportunity, imported_data: Dataset) -> P return PaymentImportStatus(seen_users, missing_users) -def _cache_key(currency_code, date=None): - return [currency_code, date.toordinal() if date else None] - - -@quickcache(vary_on=_cache_key, timeout=12 * 60 * 60) def get_exchange_rate(currency_code, date=None): # date should be a date object or None for latest rate From 1ff0ff0ce71d5a8432825e2cdeba8b7dc0edbb7a Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 7 Nov 2024 15:55:23 +0530 Subject: [PATCH 03/13] added logs --- .../opportunity/migrations/0061_exchangerate.py | 8 +++++++- commcare_connect/opportunity/visit_import.py | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/commcare_connect/opportunity/migrations/0061_exchangerate.py b/commcare_connect/opportunity/migrations/0061_exchangerate.py index 6199d473..f6ffd9df 100644 --- a/commcare_connect/opportunity/migrations/0061_exchangerate.py +++ b/commcare_connect/opportunity/migrations/0061_exchangerate.py @@ -1,16 +1,19 @@ # Generated by Django 4.2.5 on 2024-11-06 05:16 +import logging from django.db import migrations, models from django.db.models.functions import TruncDate from commcare_connect.opportunity.visit_import import get_exchange_rate +logger = logging.getLogger(__name__) + def update_exchange_rate(apps, schema_editor): Payment = apps.get_model("opportunity.Payment") payments = ( Payment.objects.annotate(date_only=TruncDate("date_paid")) - .values("date_only", "opportunity_access__opportunity__currency") + .values("id", "date_only", "amount", "opportunity_access__opportunity__currency") .distinct() ) @@ -22,6 +25,9 @@ def update_exchange_rate(apps, schema_editor): exchange_rate = 1 else: exchange_rate = get_exchange_rate(currency, date_paid) + logger.info( + f"Payment ID: {payment.id}, original USD: {payment.amount_usd}, USD acc. to new rate: {payment.amount / exchange_rate}" + ) if not exchange_rate: raise Exception(f"Invalid currency code {currency}") diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index f7976d06..798e5bb4 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -290,6 +290,7 @@ def get_exchange_rate(currency_code, date=None): return rate base_url = "https://openexchangerates.org/api" + if date: url = f"{base_url}/historical/{date.strftime('%Y-%m-%d')}.json" else: @@ -299,8 +300,9 @@ def get_exchange_rate(currency_code, date=None): rate = rates["rates"].get(currency_code) - rate_date = date if date else now().date() - ExchangeRate.objects.create(currency_code=currency_code, rate=rate, rate_date=rate_date) + if rate: + rate_date = date if date else now().date() + ExchangeRate.objects.create(currency_code=currency_code, rate=rate, rate_date=rate_date) return rate From abed581e0757ed5435d1b059d06292c7c93b9153 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Wed, 13 Nov 2024 17:25:14 +0530 Subject: [PATCH 04/13] refactor code --- commcare_connect/opportunity/visit_import.py | 48 +++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index 798e5bb4..b459bca2 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -11,6 +11,7 @@ from django.utils.timezone import now from tablib import Dataset +from commcare_connect.cache import quickcache from commcare_connect.opportunity.models import ( CatchmentArea, CompletedWork, @@ -277,47 +278,50 @@ def _bulk_update_payments(opportunity: Opportunity, imported_data: Dataset) -> P return PaymentImportStatus(seen_users, missing_users) -def get_exchange_rate(currency_code, date=None): - # date should be a date object or None for latest rate +def _cache_key(date=None): + date_key = date or now().date() + return [date_key.toordinal()] - if currency_code is None: - raise ImportException("Opportunity must have specified currency to import payments") - if currency_code == "USD": - return 1 - - rate = get_exchange_rate_from_db(currency_code, date) - if rate: - return rate +@quickcache(vary_on=_cache_key, timeout=12 * 60 * 60) +def fetch_exchange_rates(date=None): base_url = "https://openexchangerates.org/api" if date: url = f"{base_url}/historical/{date.strftime('%Y-%m-%d')}.json" else: url = f"{base_url}/latest.json" + url = f"{url}?app_id={settings.OPEN_EXCHANGE_RATES_API_ID}" rates = json.load(urllib.request.urlopen(url)) + return rates["rates"] - rate = rates["rates"].get(currency_code) - - if rate: - rate_date = date if date else now().date() - ExchangeRate.objects.create(currency_code=currency_code, rate=rate, rate_date=rate_date) - - return rate +def get_exchange_rate(currency_code, date=None): + # date should be a date object or None for latest rate -def get_exchange_rate_from_db(currency_code, date=None): + if currency_code is None: + raise ImportException("Opportunity must have specified currency to import payments") if currency_code == "USD": return 1 - if not date: - date = now().date() - + today = now().date() rate = ( - ExchangeRate.objects.filter(currency_code=currency_code, rate_date=date).values_list("rate", flat=True).first() + ExchangeRate.objects.filter(currency_code=currency_code, rate_date=date or today) + .values_list("rate", flat=True) + .first() ) + if rate: + return rate + + rates = fetch_exchange_rates(date) + rate = rates["rates"].get(currency_code) + + if rate: + rate_date = date or today + ExchangeRate.objects.create(currency_code=currency_code, rate=rate, rate_date=rate_date) + return rate From 63bc6386f07df7e3e29c2989a6870cf511f32d15 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 7 Jan 2025 16:25:02 +0530 Subject: [PATCH 05/13] fixed migration sequence and added code review fixes --- ...exchangerate_unique_currency_code_date.py} | 18 +++++++++++----- commcare_connect/opportunity/models.py | 5 +++++ commcare_connect/opportunity/visit_import.py | 21 +++++++------------ 3 files changed, 25 insertions(+), 19 deletions(-) rename commcare_connect/opportunity/migrations/{0061_exchangerate.py => 0065_exchangerate_exchangerate_unique_currency_code_date.py} (72%) diff --git a/commcare_connect/opportunity/migrations/0061_exchangerate.py b/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py similarity index 72% rename from commcare_connect/opportunity/migrations/0061_exchangerate.py rename to commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py index f6ffd9df..eb3bbcd5 100644 --- a/commcare_connect/opportunity/migrations/0061_exchangerate.py +++ b/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-11-06 05:16 +# Generated by Django 4.2.5 on 2025-01-07 10:26 import logging from django.db import migrations, models @@ -11,17 +11,19 @@ def update_exchange_rate(apps, schema_editor): Payment = apps.get_model("opportunity.Payment") + payments = ( Payment.objects.annotate(date_only=TruncDate("date_paid")) - .values("id", "date_only", "amount", "opportunity_access__opportunity__currency") + .filter(payment_unit__opportunity__currency__isnull=False) + .values("id", "date_only", "amount", "payment_unit__opportunity__currency") .distinct() ) for payment in payments: date_paid = payment["date_only"] - currency = payment["opportunity_access__opportunity__currency"] + currency = payment["payment_unit__opportunity__currency"] - if currency in ["USD", None]: + if currency is "USD": exchange_rate = 1 else: exchange_rate = get_exchange_rate(currency, date_paid) @@ -34,7 +36,7 @@ def update_exchange_rate(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("opportunity", "0060_completedwork_payment_date"), + ("opportunity", "0064_alter_completedwork_unique_together"), ] operations = [ @@ -48,5 +50,11 @@ class Migration(migrations.Migration): ("fetched_at", models.DateTimeField(auto_now_add=True)), ], ), + migrations.AddConstraint( + model_name="exchangerate", + constraint=models.UniqueConstraint( + fields=("currency_code", "rate_date"), name="unique_currency_code_date" + ), + ), migrations.RunPython(update_exchange_rate, migrations.RunPython.noop), ] diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index d1443dee..8128ddcd 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -716,3 +716,8 @@ class ExchangeRate(models.Model): rate = models.DecimalField(max_digits=10, decimal_places=6) rate_date = models.DateField() fetched_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["currency_code", "rate_date"], name="unique_currency_code_date") + ] diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index fd66cf71..1684da17 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -309,21 +309,14 @@ def get_exchange_rate(currency_code, date=None): if currency_code == "USD": return 1 - today = now().date() - rate = ( - ExchangeRate.objects.filter(currency_code=currency_code, rate_date=date or today) - .values_list("rate", flat=True) - .first() - ) + rate_date = date or now().date() + rate = None - if rate: - return rate - - rates = fetch_exchange_rates(date) - rate = rates["rates"].get(currency_code) - - if rate: - rate_date = date or today + try: + rate = ExchangeRate.objects.get(currency_code=currency_code, rate_date=rate_date).rate + except ExchangeRate.DoesNotExist: + rates = fetch_exchange_rates(rate_date) + rate = rates["rates"].get(currency_code) ExchangeRate.objects.create(currency_code=currency_code, rate=rate, rate_date=rate_date) return rate From 366c53b5646c66adeffd051b905a8d393b8bbced Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 9 Jan 2025 17:57:29 +0530 Subject: [PATCH 06/13] fix for nm_org --- ...5_exchangerate_exchangerate_unique_currency_code_date.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py b/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py index eb3bbcd5..1c866fe5 100644 --- a/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py +++ b/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py @@ -15,13 +15,15 @@ def update_exchange_rate(apps, schema_editor): payments = ( Payment.objects.annotate(date_only=TruncDate("date_paid")) .filter(payment_unit__opportunity__currency__isnull=False) - .values("id", "date_only", "amount", "payment_unit__opportunity__currency") + .values( + "id", "date_only", "amount", "opportunity_access__opportunity__currency", "invoice__opportunity__currency" + ) .distinct() ) for payment in payments: date_paid = payment["date_only"] - currency = payment["payment_unit__opportunity__currency"] + currency = payment["payment_unit__opportunity__currency"] or payment["invoice__opportunity__currency"] if currency is "USD": exchange_rate = 1 From 733c20d09b746804894c571b189284e0903ec2bb Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 9 Jan 2025 17:59:05 +0530 Subject: [PATCH 07/13] removed wrong key --- .../0065_exchangerate_exchangerate_unique_currency_code_date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py b/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py index 1c866fe5..4a35411a 100644 --- a/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py +++ b/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py @@ -23,7 +23,7 @@ def update_exchange_rate(apps, schema_editor): for payment in payments: date_paid = payment["date_only"] - currency = payment["payment_unit__opportunity__currency"] or payment["invoice__opportunity__currency"] + currency = payment["opportunity_access__opportunity__currency"] or payment["invoice__opportunity__currency"] if currency is "USD": exchange_rate = 1 From 687c241c7d257dedc2dc4315aa7eae473fbbc6e9 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 9 Jan 2025 18:55:27 +0530 Subject: [PATCH 08/13] fixed migration seq. --- ...66_exchangerate_exchangerate_unique_currency_code_date.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename commcare_connect/opportunity/migrations/{0065_exchangerate_exchangerate_unique_currency_code_date.py => 0066_exchangerate_exchangerate_unique_currency_code_date.py} (94%) diff --git a/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py similarity index 94% rename from commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py rename to commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py index 4a35411a..e54ed4e3 100644 --- a/commcare_connect/opportunity/migrations/0065_exchangerate_exchangerate_unique_currency_code_date.py +++ b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2025-01-07 10:26 +# Generated by Django 4.2.5 on 2025-01-09 13:24 import logging from django.db import migrations, models @@ -38,7 +38,7 @@ def update_exchange_rate(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("opportunity", "0064_alter_completedwork_unique_together"), + ("opportunity", "0065_uservisit_unique_xform_entity_deliver_unit"), ] operations = [ From e2b0fa001be899eff4ac82a6fa2bb7e26acfbca5 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Fri, 10 Jan 2025 10:15:06 +0530 Subject: [PATCH 09/13] removed filter condition --- .../0066_exchangerate_exchangerate_unique_currency_code_date.py | 1 - 1 file changed, 1 deletion(-) diff --git a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py index e54ed4e3..c7d0ca9c 100644 --- a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py +++ b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py @@ -14,7 +14,6 @@ def update_exchange_rate(apps, schema_editor): payments = ( Payment.objects.annotate(date_only=TruncDate("date_paid")) - .filter(payment_unit__opportunity__currency__isnull=False) .values( "id", "date_only", "amount", "opportunity_access__opportunity__currency", "invoice__opportunity__currency" ) From d39557a5ead1b25deb215d418b0401830156fe36 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Fri, 10 Jan 2025 10:53:51 +0530 Subject: [PATCH 10/13] changed log --- ...xchangerate_exchangerate_unique_currency_code_date.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py index c7d0ca9c..da54460f 100644 --- a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py +++ b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py @@ -28,9 +28,12 @@ def update_exchange_rate(apps, schema_editor): exchange_rate = 1 else: exchange_rate = get_exchange_rate(currency, date_paid) - logger.info( - f"Payment ID: {payment.id}, original USD: {payment.amount_usd}, USD acc. to new rate: {payment.amount / exchange_rate}" - ) + old_usd = payment.amount_usd + new_usd = payment.amount / exchange_rate + if old_usd and old_usd != new_usd: + logger.info( + f"Payment ID: {payment.id}, original USD: {payment.amount_usd}, USD acc. to new rate: {payment.amount / exchange_rate}" + ) if not exchange_rate: raise Exception(f"Invalid currency code {currency}") From 545d12c95f46b394402d657834bff2407b6266ba Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Fri, 10 Jan 2025 14:35:03 +0530 Subject: [PATCH 11/13] added currency conversion for orgs --- ..._exchangerate_unique_currency_code_date.py | 43 +++++++++++++------ commcare_connect/opportunity/views.py | 4 ++ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py index da54460f..3c374e09 100644 --- a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py +++ b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py @@ -12,31 +12,46 @@ def update_exchange_rate(apps, schema_editor): Payment = apps.get_model("opportunity.Payment") - payments = ( - Payment.objects.annotate(date_only=TruncDate("date_paid")) - .values( - "id", "date_only", "amount", "opportunity_access__opportunity__currency", "invoice__opportunity__currency" - ) - .distinct() + payments = Payment.objects.annotate(date_only=TruncDate("date_paid")).values( + "id", + "date_only", + "amount", + "amount_usd", + "opportunity_access__opportunity__currency", + "invoice__opportunity__currency", ) + payments_to_update = [] + for payment in payments: date_paid = payment["date_only"] - currency = payment["opportunity_access__opportunity__currency"] or payment["invoice__opportunity__currency"] + user_payment_currency = payment["opportunity_access__opportunity__currency"] + org_payment_currency = payment["invoice__opportunity__currency"] + + currency = user_payment_currency or org_payment_currency - if currency is "USD": + if currency == "USD": exchange_rate = 1 else: exchange_rate = get_exchange_rate(currency, date_paid) - old_usd = payment.amount_usd - new_usd = payment.amount / exchange_rate - if old_usd and old_usd != new_usd: - logger.info( - f"Payment ID: {payment.id}, original USD: {payment.amount_usd}, USD acc. to new rate: {payment.amount / exchange_rate}" - ) + if not exchange_rate: raise Exception(f"Invalid currency code {currency}") + old_usd = payment["amount_usd"] + new_usd = payment["amount"] / exchange_rate + + # Log discrepancies if old_usd exists and is different from new_usd + if old_usd and old_usd != new_usd: + logger.info(f"Payment ID: {payment['id']}, original USD: {old_usd}, USD acc. to new rate: {new_usd}") + + # Update only for org payments + if old_usd is None and user_payment_currency is None and org_payment_currency: + payments_to_update.append(Payment(id=payment["id"], amount_usd=new_usd)) + + if payments_to_update: + Payment.objects.bulk_update(payments_to_update, ["amount_usd"], batch_size=1000) + class Migration(migrations.Migration): dependencies = [ diff --git a/commcare_connect/opportunity/views.py b/commcare_connect/opportunity/views.py index 2cef88ed..e27a2830 100644 --- a/commcare_connect/opportunity/views.py +++ b/commcare_connect/opportunity/views.py @@ -105,6 +105,7 @@ bulk_update_completed_work_status, bulk_update_payment_status, bulk_update_visit_status, + get_exchange_rate, update_payment_accrued, ) from commcare_connect.organization.decorators import org_admin_required, org_member_required, org_viewer_required @@ -1221,10 +1222,13 @@ def invoice_approve(request, org_slug, pk): return redirect("opportunity:detail", org_slug, pk) invoice_ids = request.POST.getlist("pk") invoices = PaymentInvoice.objects.filter(opportunity=opportunity, pk__in=invoice_ids, payment__isnull=True) + rate = get_exchange_rate(opportunity.currency) for invoice in invoices: + amount_in_usd = invoice.amount / rate payment = Payment( amount=invoice.amount, organization=opportunity.organization, + amount_usd=amount_in_usd, invoice=invoice, ) payment.save() From 22b86ed777ac9b7e90585498c96fb3454f69dc74 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 14 Jan 2025 17:17:10 +0530 Subject: [PATCH 12/13] added distinct and update payment amount in usd --- ..._exchangerate_unique_currency_code_date.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py index 3c374e09..796ae575 100644 --- a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py +++ b/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py @@ -12,17 +12,19 @@ def update_exchange_rate(apps, schema_editor): Payment = apps.get_model("opportunity.Payment") - payments = Payment.objects.annotate(date_only=TruncDate("date_paid")).values( - "id", - "date_only", - "amount", - "amount_usd", - "opportunity_access__opportunity__currency", - "invoice__opportunity__currency", + payments = ( + Payment.objects.annotate(date_only=TruncDate("date_paid")) + .values( + "id", + "date_only", + "amount", + "amount_usd", + "opportunity_access__opportunity__currency", + "invoice__opportunity__currency", + ) + .distinct() ) - payments_to_update = [] - for payment in payments: date_paid = payment["date_only"] user_payment_currency = payment["opportunity_access__opportunity__currency"] @@ -42,15 +44,19 @@ def update_exchange_rate(apps, schema_editor): new_usd = payment["amount"] / exchange_rate # Log discrepancies if old_usd exists and is different from new_usd - if old_usd and old_usd != new_usd: - logger.info(f"Payment ID: {payment['id']}, original USD: {old_usd}, USD acc. to new rate: {new_usd}") + if old_usd: + if old_usd != new_usd: + logger.info(f"Payment ID: {payment['id']}, original USD: {old_usd}, USD acc. to new rate: {new_usd}") + continue + + payments_to_update = Payment.objects.filter(date_paid=date_paid, amount_usd=None) - # Update only for org payments - if old_usd is None and user_payment_currency is None and org_payment_currency: - payments_to_update.append(Payment(id=payment["id"], amount_usd=new_usd)) + if user_payment_currency: + payments_to_update = payments_to_update.filter(opportunity_access__opportunity__currency=currency) + else: + payments_to_update = payments_to_update.filter(invoice__opportunity__currency=currency) - if payments_to_update: - Payment.objects.bulk_update(payments_to_update, ["amount_usd"], batch_size=1000) + payments_to_update.update(amount_usd=models.F("amount") / exchange_rate) class Migration(migrations.Migration): From a344744d21071d2cfe658ba3e212716fdf0ff62d Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 14 Jan 2025 18:27:44 +0530 Subject: [PATCH 13/13] fixed migration sequence --- ...67_exchangerate_exchangerate_unique_currency_code_date.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename commcare_connect/opportunity/migrations/{0066_exchangerate_exchangerate_unique_currency_code_date.py => 0067_exchangerate_exchangerate_unique_currency_code_date.py} (95%) diff --git a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py b/commcare_connect/opportunity/migrations/0067_exchangerate_exchangerate_unique_currency_code_date.py similarity index 95% rename from commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py rename to commcare_connect/opportunity/migrations/0067_exchangerate_exchangerate_unique_currency_code_date.py index 796ae575..16a02c18 100644 --- a/commcare_connect/opportunity/migrations/0066_exchangerate_exchangerate_unique_currency_code_date.py +++ b/commcare_connect/opportunity/migrations/0067_exchangerate_exchangerate_unique_currency_code_date.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2025-01-09 13:24 +# Generated by Django 4.2.5 on 2025-01-14 12:55 import logging from django.db import migrations, models @@ -61,7 +61,7 @@ def update_exchange_rate(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("opportunity", "0065_uservisit_unique_xform_entity_deliver_unit"), + ("opportunity", "0066_completedwork_date_created_uservisit_date_created"), ] operations = [