diff --git a/.circleci/config.yml b/.circleci/config.yml index 9d5fb4f253..27b5386da4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,7 +49,7 @@ jobs: environment: PGHOST: 127.0.0.1 DATABASE_URL: "postgis://postgres:postgres@localhost:5432/circle_test" - DEPLOY_BRANCHES: "develop|staging|master|ci-updates2|epd-amp|32722-amp-changes-and-fixes" + DEPLOY_BRANCHES: "develop|staging|master|ci-updates2|ch33354-UNICEF-cash-unfunded" - image: cimg/postgres:12.9-postgis environment: POSTGRES_USER: postgres diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index b46b8b5e72..888c7ca21b 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -131,8 +131,10 @@ class InterventionBudgetAdmin(RestrictedEditAdmin): fields = ( 'intervention', 'currency', + 'has_unfunded_cash', 'partner_contribution', 'unicef_cash', + 'total_unfunded', 'in_kind_amount', 'partner_contribution_local', 'unicef_cash_local', diff --git a/src/etools/applications/partners/amendment_utils.py b/src/etools/applications/partners/amendment_utils.py index 50b0bc0207..1a5d9f48c2 100644 --- a/src/etools/applications/partners/amendment_utils.py +++ b/src/etools/applications/partners/amendment_utils.py @@ -677,6 +677,7 @@ def full_snapshot_instance(instance, relations_to_copy, exclude_fields): 'in_kind_amount_local', 'total', 'total_local', + 'total_unfunded', 'programme_effectiveness', ], 'partners.InterventionManagementBudget': ['modified'], diff --git a/src/etools/applications/partners/locale/ar/LC_MESSAGES/django.mo b/src/etools/applications/partners/locale/ar/LC_MESSAGES/django.mo index a1a472f322..8beb2161f2 100644 Binary files a/src/etools/applications/partners/locale/ar/LC_MESSAGES/django.mo and b/src/etools/applications/partners/locale/ar/LC_MESSAGES/django.mo differ diff --git a/src/etools/applications/partners/locale/ar/LC_MESSAGES/django.po b/src/etools/applications/partners/locale/ar/LC_MESSAGES/django.po index 394989abd3..aa9eb582b6 100644 --- a/src/etools/applications/partners/locale/ar/LC_MESSAGES/django.po +++ b/src/etools/applications/partners/locale/ar/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-14 11:43+0000\n" +"POT-Creation-Date: 2024-01-16 13:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1147,6 +1147,9 @@ msgstr "يونيسيف كاش" msgid "UNICEF Supplies" msgstr "لوازم اليونيسف" +msgid "Total Unfunded" +msgstr "" + msgid "Partner Contribution Local" msgstr "مساهمة الشريك بالعملة المحلية" @@ -1159,12 +1162,18 @@ msgstr "إجمالي مساهمة الشريك" msgid "Total HQ Cash Local" msgstr "إجمالي النقد المحلي للمقر الرئيسي" +msgid "Unfunded Capacity Strengthening Cash Local" +msgstr "تكاليف تعزيز القدرات غير ممول" + msgid "Unicef Cash Local" msgstr "منظمة اليونيسف النقدية المحلية" msgid "UNICEF Supplies Local" msgstr "مستلزمات اليونيسف محلية" +msgid "Unfunded Cash" +msgstr "" + msgid "Currency" msgstr "عملة" @@ -1369,6 +1378,12 @@ msgstr "" "مساهمة الشريك في الإدارة الداخلية وموظفي الدعم مقسمة إلى مساهماتهم في " "البرنامج (التمثيل ، التخطيط ، التنسيق ، اللوجستيات ، الإدارة ، المالية)" +msgid "" +"Unfunded amount for In-country management and support staff prorated to " +"their contribution to the programme (representation, planning, coordination, " +"logistics, administration, finance)" +msgstr "" + msgid "" "UNICEF contribution for Operational costs prorated to their contribution to " "the programme (office space, equipment, office supplies, maintenance)" @@ -1383,6 +1398,11 @@ msgstr "" "مساهمة الشركاء في التكاليف التشغيلية مقسمة بالتناسب مع مساهمتهم في البرنامج " "(مساحة مكتبية ، معدات ، لوازم مكتبية ، صيانة)" +msgid "" +"Unfunded amount for Operational costs prorated to their contribution to the " +"programme (office space, equipment, office supplies, maintenance)" +msgstr "" + msgid "" "UNICEF contribution for Planning, monitoring, evaluation and communication, " "prorated to their contribution to the programme (venue, travels, etc.)" @@ -1397,6 +1417,11 @@ msgstr "" "مساهمة الشريك في التخطيط والمراقبة والتقييم والاتصال ، مُتناسبة مع مساهمتهم " "في البرنامج (المكان ، والسفر ، وما إلى ذلك)" +msgid "" +"Unfunded amount for Planning, monitoring, evaluation and communication, " +"prorated to their contribution to the programme (venue, travels, etc.)" +msgstr "" + msgid "Title" msgstr "عنوان" @@ -1446,6 +1471,9 @@ msgstr "منظمة اليونيسف النقدية المحلية" msgid "CSO Cash Local" msgstr "مساهمة منظمة المجتمع المدني النقدية" +msgid "Unfunded Cash Local" +msgstr "" + msgid "Accessing this item is not allowed." msgstr "الوصول إلى هذا العنصر غير مسموح به." @@ -1533,6 +1561,15 @@ msgstr "تصنيف مخاطر الإستغلال و الإنتهاك الجنس msgid "HACT" msgstr "اطار النهج المنسق للتحويلات النقدية" +msgid "" +"This programme document has unfunded amounts. Please fix them before " +"deactivating." +msgstr "" +".تحتوي وثيقة البرنامج هذه على مبالغ غير ممولة. يرجى إصلاحها قبل التعطيل" + +msgid "This programme document does not include unfunded amounts." +msgstr ".لا تتضمن وثيقة البرنامج هذه المبالغ غير الممولة" + msgid "" "Cannot add a new amendment while another amendment of same kind is in " "progress." @@ -1662,8 +1699,10 @@ msgstr "يجب أن ينتمي رقم البائع إلى مجموعة حساب msgid "Unknown intervention." msgstr "تدخل غير معروف." +#, fuzzy +#| msgid "Signatures cannot be dated in the future" msgid "Results cannot be changed in this status" -msgstr "لا يمكن تغيير النتائج في هذه الحالة" +msgstr "لا يمكن تأريخ التوقيعات في المستقبل" msgid "EZHACT Vision integration disabled" msgstr "تم تعطيل تكامل مع برنامج vision- EZHACT" @@ -1923,8 +1962,10 @@ msgstr "لا يمكن تنشيط وثيقة البرامج إذا كان الش msgid "Agreement selected is not of type SSFA" msgstr "الاتفاقية المختارة ليست من نوع اتفاقية التمويل الصغير" +#, fuzzy +#| msgid " without deleting the indicators first" msgid " without deleting the indicators first " -msgstr " دون حذف المؤشرات أولا" +msgstr "دون حذف المؤشرات أولا" #, python-format msgid "" diff --git a/src/etools/applications/partners/locale/es/LC_MESSAGES/django.mo b/src/etools/applications/partners/locale/es/LC_MESSAGES/django.mo index 8e915f79a1..9b4992875a 100644 Binary files a/src/etools/applications/partners/locale/es/LC_MESSAGES/django.mo and b/src/etools/applications/partners/locale/es/LC_MESSAGES/django.mo differ diff --git a/src/etools/applications/partners/locale/es/LC_MESSAGES/django.po b/src/etools/applications/partners/locale/es/LC_MESSAGES/django.po index d5f5683805..3a2cf3b8a3 100644 --- a/src/etools/applications/partners/locale/es/LC_MESSAGES/django.po +++ b/src/etools/applications/partners/locale/es/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: et-partners2-bk\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-14 11:43+0000\n" +"POT-Creation-Date: 2024-01-16 13:17+0000\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1144,6 +1144,9 @@ msgstr "UNICEF Efectivo" msgid "UNICEF Supplies" msgstr "Suministros UNICEF" +msgid "Total Unfunded" +msgstr "" + msgid "Partner Contribution Local" msgstr "Socio Contribución Local" @@ -1156,12 +1159,18 @@ msgstr "Contribución total de los socios" msgid "Total HQ Cash Local" msgstr "Total Sede Efectivo Local" +msgid "Unfunded Capacity Strengthening Cash Local" +msgstr "Costos de fortalecimiento de capacidades sin fondos" + msgid "Unicef Cash Local" msgstr "Unicef Cash Local" msgid "UNICEF Supplies Local" msgstr "UNICEF Suministros locales" +msgid "Unfunded Cash" +msgstr "" + msgid "Currency" msgstr "Moneda" @@ -1369,6 +1378,12 @@ msgstr "" "prorrateada en función de su contribución al programa (representación, " "planificación, coordinación, logística, administración, finanzas)." +msgid "" +"Unfunded amount for In-country management and support staff prorated to " +"their contribution to the programme (representation, planning, coordination, " +"logistics, administration, finance)" +msgstr "" + msgid "" "UNICEF contribution for Operational costs prorated to their contribution to " "the programme (office space, equipment, office supplies, maintenance)" @@ -1385,6 +1400,11 @@ msgstr "" "su contribución al programa (espacio de oficinas, equipamiento, material de " "oficina, mantenimiento)." +msgid "" +"Unfunded amount for Operational costs prorated to their contribution to the " +"programme (office space, equipment, office supplies, maintenance)" +msgstr "" + msgid "" "UNICEF contribution for Planning, monitoring, evaluation and communication, " "prorated to their contribution to the programme (venue, travels, etc.)" @@ -1401,6 +1421,11 @@ msgstr "" "comunicación, prorrateada en función de su contribución al programa (lugar " "de celebración, viajes, etc.)" +msgid "" +"Unfunded amount for Planning, monitoring, evaluation and communication, " +"prorated to their contribution to the programme (venue, travels, etc.)" +msgstr "" + msgid "Title" msgstr "Título" @@ -1451,6 +1476,9 @@ msgstr "Unicef Cash Local" msgid "CSO Cash Local" msgstr "CSO Efectivo Local" +msgid "Unfunded Cash Local" +msgstr "" + msgid "Accessing this item is not allowed." msgstr "No está permitido acceder a este elemento." @@ -1538,6 +1566,16 @@ msgstr "Clasificación de riesgo de la EAE" msgid "HACT" msgstr "Hact" +msgid "" +"This programme document has unfunded amounts. Please fix them before " +"deactivating." +msgstr "" +"Este documento de programa tiene montos no financiados. Corríjalos antes de " +"desactivarlos." + +msgid "This programme document does not include unfunded amounts." +msgstr "Este documento de programa no incluye montos no financiados." + msgid "" "Cannot add a new amendment while another amendment of same kind is in " "progress." @@ -1695,8 +1733,10 @@ msgstr "El número de proveedor debe pertenecer al grupo de cuentas PRG2" msgid "Unknown intervention." msgstr "Intervención desconocida." +#, fuzzy +#| msgid "Signatures cannot be dated in the future" msgid "Results cannot be changed in this status" -msgstr "Los resultados no se pueden cambiar en este estado" +msgstr "Las firmas no pueden tener fecha futura" msgid "EZHACT Vision integration disabled" msgstr "Integración de EZHACT Vision desactivada" @@ -2307,25 +2347,17 @@ msgstr "" #~ msgid "Last Name" #~ msgstr "Apellido" -#, fuzzy -#~| msgid "Partner Authorized Officer" #~ msgid "(old)Partner Authorized Officer" -#~ msgstr "Socio Funcionario autorizado" +#~ msgstr "(old)Socio Funcionario autorizado" -#, fuzzy -#~| msgid "Signed by partner" #~ msgid "(old)Signed by partner" -#~ msgstr "Firmado por el socio" +#~ msgstr "(old)Firmado por el socio" -#, fuzzy -#~| msgid "Signed by Partner" #~ msgid "(old)Signed by Partner" -#~ msgstr "Firmado por el socio" +#~ msgstr "(old)Firmado por el socio" -#, fuzzy -#~| msgid "CSO Authorized Officials" #~ msgid "(old)CSO Authorized Officials" -#~ msgstr "CSO Funcionarios autorizados" +#~ msgstr "(old)CSO Funcionarios autorizados" #~ msgid "" #~ "User already synced to PRP and cannot be disabled. Please instruct the " diff --git a/src/etools/applications/partners/locale/fr/LC_MESSAGES/django.mo b/src/etools/applications/partners/locale/fr/LC_MESSAGES/django.mo index c5ab90672d..31cda95cc9 100644 Binary files a/src/etools/applications/partners/locale/fr/LC_MESSAGES/django.mo and b/src/etools/applications/partners/locale/fr/LC_MESSAGES/django.mo differ diff --git a/src/etools/applications/partners/locale/fr/LC_MESSAGES/django.po b/src/etools/applications/partners/locale/fr/LC_MESSAGES/django.po index d4344708cc..b776283c1f 100644 --- a/src/etools/applications/partners/locale/fr/LC_MESSAGES/django.po +++ b/src/etools/applications/partners/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-14 11:43+0000\n" +"POT-Creation-Date: 2024-01-16 13:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1153,6 +1153,9 @@ msgstr "Unicef Cash" msgid "UNICEF Supplies" msgstr "Fournitures de l'UNICEF" +msgid "Total Unfunded" +msgstr "" + msgid "Partner Contribution Local" msgstr "Contribution du partenaire Local" @@ -1165,12 +1168,18 @@ msgstr "Contribution totale des partenaires" msgid "Total HQ Cash Local" msgstr "Total HQ Cash Local" +msgid "Unfunded Capacity Strengthening Cash Local" +msgstr "Coûts de renforcement des capacités non financé" + msgid "Unicef Cash Local" msgstr "Unicef Cash Local" msgid "UNICEF Supplies Local" msgstr "Fournitures UNICEF locales" +msgid "Unfunded Cash" +msgstr "" + msgid "Currency" msgstr "Monnaie" @@ -1379,6 +1388,12 @@ msgstr "" "le pays au prorata de leur contribution au programme (représentation, " "planification, coordination, logistique, administration, finances)." +msgid "" +"Unfunded amount for In-country management and support staff prorated to " +"their contribution to the programme (representation, planning, coordination, " +"logistics, administration, finance)" +msgstr "" + msgid "" "UNICEF contribution for Operational costs prorated to their contribution to " "the programme (office space, equipment, office supplies, maintenance)" @@ -1395,6 +1410,11 @@ msgstr "" "contribution au programme (espace de bureau, équipement, fournitures de " "bureau, entretien)." +msgid "" +"Unfunded amount for Operational costs prorated to their contribution to the " +"programme (office space, equipment, office supplies, maintenance)" +msgstr "" + msgid "" "UNICEF contribution for Planning, monitoring, evaluation and communication, " "prorated to their contribution to the programme (venue, travels, etc.)" @@ -1411,6 +1431,11 @@ msgstr "" "et la communication, au prorata de leur contribution au programme (lieu, " "voyages, etc.)." +msgid "" +"Unfunded amount for Planning, monitoring, evaluation and communication, " +"prorated to their contribution to the programme (venue, travels, etc.)" +msgstr "" + msgid "Title" msgstr "Titre" @@ -1461,6 +1486,9 @@ msgstr "Unicef Cash Local" msgid "CSO Cash Local" msgstr "CSO Cash Local" +msgid "Unfunded Cash Local" +msgstr "" + msgid "Accessing this item is not allowed." msgstr "L'accès à cet élément n'est pas autorisé." @@ -1548,6 +1576,16 @@ msgstr "Cote de risque SEA" msgid "HACT" msgstr "Hact" +msgid "" +"This programme document has unfunded amounts. Please fix them before " +"deactivating." +msgstr "" +"Ce document de programme comporte des montants non financés. Veuillez les " +"corriger avant de désactiver." + +msgid "This programme document does not include unfunded amounts." +msgstr "Ce document de programme n'inclut pas les montants non financés." + msgid "" "Cannot add a new amendment while another amendment of same kind is in " "progress." @@ -1701,8 +1739,10 @@ msgstr "Le numéro de fournisseur doit appartenir au groupe de comptes PRG2" msgid "Unknown intervention." msgstr "Intervention inconnue." +#, fuzzy +#| msgid "Signatures cannot be dated in the future" msgid "Results cannot be changed in this status" -msgstr "Les résultats ne peuvent pas être modifiés dans cet état" +msgstr "Les signatures ne peuvent pas être datées dans le futur" msgid "EZHACT Vision integration disabled" msgstr "Intégration d'EZHACT Vision désactivée" @@ -1975,8 +2015,10 @@ msgstr "Le DP ne peut pas être activé si le partenaire est bloqué en vision." msgid "Agreement selected is not of type SSFA" msgstr "L'accord sélectionné n'est pas de type SSFA" +#, fuzzy +#| msgid " without deleting the indicators first" msgid " without deleting the indicators first " -msgstr "sans supprimer les indicateurs au préalable " +msgstr "sans supprimer les indicateurs au préalable" #, python-format msgid "" @@ -2322,25 +2364,17 @@ msgstr "Rôle de gestionnaire de partenariat requis pour l'exportation pca." #~ msgid "Last Name" #~ msgstr "Nom de famille" -#, fuzzy -#~| msgid "Partner Authorized Officer" #~ msgid "(old)Partner Authorized Officer" -#~ msgstr "Partenaire Agent autorisé" +#~ msgstr "(old)Partenaire Agent autorisé" -#, fuzzy -#~| msgid "Signed by partner" #~ msgid "(old)Signed by partner" -#~ msgstr "Signé par le partenaire" +#~ msgstr "(old)Signé par le partenaire" -#, fuzzy -#~| msgid "Signed by Partner" #~ msgid "(old)Signed by Partner" -#~ msgstr "Signé par le partenaire" +#~ msgstr "(old)Signé par le partenaire" -#, fuzzy -#~| msgid "CSO Authorized Officials" #~ msgid "(old)CSO Authorized Officials" -#~ msgstr "Fonctionnaires autorisés de l'OSC" +#~ msgstr "(old)Fonctionnaires autorisés de l'OSC" #~ msgid "" #~ "User already synced to PRP and cannot be disabled. Please instruct the " diff --git a/src/etools/applications/partners/locale/pt/LC_MESSAGES/django.mo b/src/etools/applications/partners/locale/pt/LC_MESSAGES/django.mo index 6cfcf0615d..99622655a7 100644 Binary files a/src/etools/applications/partners/locale/pt/LC_MESSAGES/django.mo and b/src/etools/applications/partners/locale/pt/LC_MESSAGES/django.mo differ diff --git a/src/etools/applications/partners/locale/pt/LC_MESSAGES/django.po b/src/etools/applications/partners/locale/pt/LC_MESSAGES/django.po index 6801d2ae70..0bcc28559d 100644 --- a/src/etools/applications/partners/locale/pt/LC_MESSAGES/django.po +++ b/src/etools/applications/partners/locale/pt/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-14 11:43+0000\n" +"POT-Creation-Date: 2024-01-16 13:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1165,6 +1165,9 @@ msgstr "Valor de transferências financeiras (UNICEF)" msgid "UNICEF Supplies" msgstr "Suprimentos do UNICEF" +msgid "Total Unfunded" +msgstr "" + msgid "Partner Contribution Local" msgstr "Contribuição local do parceiro" @@ -1177,12 +1180,18 @@ msgstr "Contribuição total do parceiro" msgid "Total HQ Cash Local" msgstr "Total da contribuição financeira local para a sede" +msgid "Unfunded Capacity Strengthening Cash Local" +msgstr "Custos de Fortalecimento da Capacidade sem financiamento" + msgid "Unicef Cash Local" msgstr "Unicef Valor financeiro local" msgid "UNICEF Supplies Local" msgstr "Suprimentos UNICEF local" +msgid "Unfunded Cash" +msgstr "" + msgid "Currency" msgstr "Moeda" @@ -1391,6 +1400,12 @@ msgstr "" "pessoal proporcional à sua contribuição ao programa (representação, " "planejamento, coordenação, logística, administração, finanças)" +msgid "" +"Unfunded amount for In-country management and support staff prorated to " +"their contribution to the programme (representation, planning, coordination, " +"logistics, administration, finance)" +msgstr "" + msgid "" "UNICEF contribution for Operational costs prorated to their contribution to " "the programme (office space, equipment, office supplies, maintenance)" @@ -1407,6 +1422,11 @@ msgstr "" "contribuição para o programa (espaço de escritório, equipamento, material de " "escritório, manutenção)" +msgid "" +"Unfunded amount for Operational costs prorated to their contribution to the " +"programme (office space, equipment, office supplies, maintenance)" +msgstr "" + msgid "" "UNICEF contribution for Planning, monitoring, evaluation and communication, " "prorated to their contribution to the programme (venue, travels, etc.)" @@ -1423,6 +1443,11 @@ msgstr "" "comunicação, proporcional à sua contribuição ao programa (local, viagens, " "etc.)" +msgid "" +"Unfunded amount for Planning, monitoring, evaluation and communication, " +"prorated to their contribution to the programme (venue, travels, etc.)" +msgstr "" + msgid "Title" msgstr "Título" @@ -1475,6 +1500,9 @@ msgstr "Unicef Cash Local" msgid "CSO Cash Local" msgstr "OSC Valor contribuição" +msgid "Unfunded Cash Local" +msgstr "" + msgid "Accessing this item is not allowed." msgstr "Não é permitido acessar este item." @@ -1562,6 +1590,16 @@ msgstr "Classificação de risco SEA" msgid "HACT" msgstr "HACT" +msgid "" +"This programme document has unfunded amounts. Please fix them before " +"deactivating." +msgstr "" +"Este documento do programa tem valores não financiados. Corrija-os antes de " +"desativar." + +msgid "This programme document does not include unfunded amounts." +msgstr "Este documento do programa não inclui valores não financiados." + msgid "" "Cannot add a new amendment while another amendment of same kind is in " "progress." @@ -1712,8 +1750,10 @@ msgstr "O número do fornecedor deve pertencer ao grupo de contas PRG2" msgid "Unknown intervention." msgstr "Intervenção desconhecida." +#, fuzzy +#| msgid "Signatures cannot be dated in the future" msgid "Results cannot be changed in this status" -msgstr "Os resultados não podem ser alterados neste estado" +msgstr "As assinaturas não podem ser datadas no futuro" msgid "EZHACT Vision integration disabled" msgstr "Integração com o EZHACT Vision desativada" @@ -1984,8 +2024,10 @@ msgstr "O PD não pode ser ativado se o parceiro estiver bloqueado no VISION." msgid "Agreement selected is not of type SSFA" msgstr "O contrato selecionado não é do tipo SSFA" +#, fuzzy +#| msgid " without deleting the indicators first" msgid " without deleting the indicators first " -msgstr "sem excluir os indicadores primeiro " +msgstr "sem excluir os indicadores primeiro" #, python-format msgid "" @@ -2328,26 +2370,19 @@ msgstr "A função de gerente de parceria é necessária para a exportação de #~ msgid "Last Name" #~ msgstr "Sobrenome" -#, fuzzy -#~| msgid "Partner Authorized Officer" #~ msgid "(old)Partner Authorized Officer" -#~ msgstr "Oficial autorizado do parceiro" +#~ msgstr "(old)Oficial autorizado do parceiro" -#, fuzzy -#~| msgid "Signed by partner" #~ msgid "(old)Signed by partner" -#~ msgstr "Assinado pelo parceiro" +#~ msgstr "(old)Assinado pelo parceiro" -#, fuzzy -#~| msgid "Signed by Partner" #~ msgid "(old)Signed by Partner" -#~ msgstr "Assinado pelo parceiro" +#~ msgstr "(old)Assinado pelo parceiro" -#, fuzzy -#~| msgid "CSO Authorized Officials" #~ msgid "(old)CSO Authorized Officials" #~ msgstr "" -#~ "Oficiais da OSC (organização da Sociedade Civil) Autorizados a assinar" +#~ "(old)Oficiais da OSC (organização da Sociedade Civil) Autorizados a " +#~ "assinar" #~ msgid "" #~ "User already synced to PRP and cannot be disabled. Please instruct the " diff --git a/src/etools/applications/partners/locale/ru/LC_MESSAGES/django.mo b/src/etools/applications/partners/locale/ru/LC_MESSAGES/django.mo index e0bcda84ca..00e3e7bc25 100644 Binary files a/src/etools/applications/partners/locale/ru/LC_MESSAGES/django.mo and b/src/etools/applications/partners/locale/ru/LC_MESSAGES/django.mo differ diff --git a/src/etools/applications/partners/locale/ru/LC_MESSAGES/django.po b/src/etools/applications/partners/locale/ru/LC_MESSAGES/django.po index 6e7c3ba291..9e4b33f642 100644 --- a/src/etools/applications/partners/locale/ru/LC_MESSAGES/django.po +++ b/src/etools/applications/partners/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-14 11:43+0000\n" +"POT-Creation-Date: 2024-01-16 13:17+0000\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1146,6 +1146,9 @@ msgstr "Денежные средства ЮНИСЕФ" msgid "UNICEF Supplies" msgstr "Снабжение ЮНИСЕФ" +msgid "Total Unfunded" +msgstr "" + msgid "Partner Contribution Local" msgstr "Вклад партнера (в местной валюте)" @@ -1158,12 +1161,18 @@ msgstr "Общий вклад партнера" msgid "Total HQ Cash Local" msgstr "Итого взнос на укрепление потенциала (в местной валюте)" +msgid "Unfunded Capacity Strengthening Cash Local" +msgstr "Затраты на укрепление потенциала необеспеченный" + msgid "Unicef Cash Local" msgstr "Unicef Cash Local" msgid "UNICEF Supplies Local" msgstr "Снабжение ЮНИСЕФ (в местной валюте)" +msgid "Unfunded Cash" +msgstr "" + msgid "Currency" msgstr "Валюта" @@ -1371,6 +1380,12 @@ msgstr "" "пропорциональный их вкладу в программу (представительство, планирование, " "координация, материально-техническое обеспечение, администрирование, финансы)" +msgid "" +"Unfunded amount for In-country management and support staff prorated to " +"their contribution to the programme (representation, planning, coordination, " +"logistics, administration, finance)" +msgstr "" + msgid "" "UNICEF contribution for Operational costs prorated to their contribution to " "the programme (office space, equipment, office supplies, maintenance)" @@ -1387,6 +1402,11 @@ msgstr "" "помещения, оборудование, канцелярские принадлежности, техническое " "обслуживание)" +msgid "" +"Unfunded amount for Operational costs prorated to their contribution to the " +"programme (office space, equipment, office supplies, maintenance)" +msgstr "" + msgid "" "UNICEF contribution for Planning, monitoring, evaluation and communication, " "prorated to their contribution to the programme (venue, travels, etc.)" @@ -1401,6 +1421,11 @@ msgstr "" "Вклад партнеров в планирование, мониторинг, оценку и коммуникацию, " "пропорционально программе (место проведения, поездки и т.д.)" +msgid "" +"Unfunded amount for Planning, monitoring, evaluation and communication, " +"prorated to their contribution to the programme (venue, travels, etc.)" +msgstr "" + msgid "Title" msgstr "Название" @@ -1452,6 +1477,9 @@ msgstr "Денежный взнос ЮНИСЕФ (в местной валюте msgid "CSO Cash Local" msgstr "Денежный взнос ОГО (в местной валюте)" +msgid "Unfunded Cash Local" +msgstr "" + msgid "Accessing this item is not allowed." msgstr "Доступ к этому элементу запрещен." @@ -1539,6 +1567,16 @@ msgstr "Рейтинг риска СЭН" msgid "HACT" msgstr "ГПДП" +msgid "" +"This programme document has unfunded amounts. Please fix them before " +"deactivating." +msgstr "" +"Этот программный документ имеет необеспеченные суммы. Пожалуйста, исправьте " +"их перед деактивацией." + +msgid "This programme document does not include unfunded amounts." +msgstr "Этот программный документ не включает необеспеченные суммы." + msgid "" "Cannot add a new amendment while another amendment of same kind is in " "progress." @@ -1686,8 +1724,10 @@ msgstr "Номер поставщика должен принадлежать к msgid "Unknown intervention." msgstr "Неизвестное вмешательство." +#, fuzzy +#| msgid "Signatures cannot be dated in the future" msgid "Results cannot be changed in this status" -msgstr "Результаты не могут быть изменены в этом статусе" +msgstr "Подписи не могут быть датированы в будущем" msgid "EZHACT Vision integration disabled" msgstr "Интеграция с EZHACT Vision отключена" @@ -2291,25 +2331,17 @@ msgstr "Для экспорта PCA требуется роль менеджер #~ msgid "Last Name" #~ msgstr "Фамилия" -#, fuzzy -#~| msgid "Partner Authorized Officer" #~ msgid "(old)Partner Authorized Officer" -#~ msgstr "Партнер Уполномоченный сотрудник" +#~ msgstr "(old)Партнер Уполномоченный сотрудник" -#, fuzzy -#~| msgid "Signed by partner" #~ msgid "(old)Signed by partner" -#~ msgstr "Подписано партнером" +#~ msgstr "(old)Подписано партнером" -#, fuzzy -#~| msgid "Signed by Partner" #~ msgid "(old)Signed by Partner" -#~ msgstr "Подписано партнером" +#~ msgstr "(old)Подписано партнером" -#, fuzzy -#~| msgid "CSO Authorized Officials" #~ msgid "(old)CSO Authorized Officials" -#~ msgstr "Уполномоченные должностные лица CSO" +#~ msgstr "(old)Уполномоченные должностные лица CSO" #~ msgid "" #~ "User already synced to PRP and cannot be disabled. Please instruct the " diff --git a/src/etools/applications/partners/migrations/0121_auto_20230614_0841.py b/src/etools/applications/partners/migrations/0121_auto_20230614_0841.py new file mode 100644 index 0000000000..a6ce2f2e4d --- /dev/null +++ b/src/etools/applications/partners/migrations/0121_auto_20230614_0841.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.6 on 2023-06-14 08:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0120_merge_20230502_1523'), + ] + + operations = [ + migrations.AddField( + model_name='interventionbudget', + name='has_unfunded_cash', + field=models.BooleanField(default=False, verbose_name='Unfunded Cash'), + ), + migrations.AddField( + model_name='interventionbudget', + name='total_unfunded', + field=models.DecimalField(decimal_places=2, default=0, max_digits=20, verbose_name='Total Unfunded'), + ), + migrations.AddField( + model_name='interventionbudget', + name='unfunded_hq_cash', + field=models.DecimalField(decimal_places=2, default=0, max_digits=20, verbose_name='Unfunded Capacity Strengthening Cash Local'), + ), + migrations.AddField( + model_name='interventionmanagementbudget', + name='act1_unfunded', + field=models.DecimalField(decimal_places=2, default=0, max_digits=20, verbose_name='Unfunded amount for In-country management and support staff prorated to their contribution to the programme (representation, planning, coordination, logistics, administration, finance)'), + ), + migrations.AddField( + model_name='interventionmanagementbudget', + name='act2_unfunded', + field=models.DecimalField(decimal_places=2, default=0, max_digits=20, verbose_name='Unfunded amount for Operational costs prorated to their contribution to the programme (office space, equipment, office supplies, maintenance)'), + ), + migrations.AddField( + model_name='interventionmanagementbudget', + name='act3_unfunded', + field=models.DecimalField(decimal_places=2, default=0, max_digits=20, verbose_name='Unfunded amount for Planning, monitoring, evaluation and communication, prorated to their contribution to the programme (venue, travels, etc.)'), + ), + migrations.AddField( + model_name='interventionmanagementbudgetitem', + name='unfunded_cash', + field=models.DecimalField(decimal_places=2, default=0, max_digits=20, verbose_name='Unfunded Cash Local'), + ), + ] diff --git a/src/etools/applications/partners/migrations/0122_merge_0121_auto_20230614_0841_0121_auto_20230814_1058.py b/src/etools/applications/partners/migrations/0122_merge_0121_auto_20230614_0841_0121_auto_20230814_1058.py new file mode 100644 index 0000000000..568fd4720b --- /dev/null +++ b/src/etools/applications/partners/migrations/0122_merge_0121_auto_20230614_0841_0121_auto_20230814_1058.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2.19 on 2024-01-16 12:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0121_auto_20230614_0841'), + ('partners', '0121_auto_20230814_1058'), + ] + + operations = [ + ] diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 52821ebe69..6d76c5bfb0 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -2911,7 +2911,8 @@ def total(self): results = self.ll_results.filter().aggregate( total=( Sum("activities__unicef_cash", filter=Q(activities__is_active=True)) + - Sum("activities__cso_cash", filter=Q(activities__is_active=True)) + Sum("activities__cso_cash", filter=Q(activities__is_active=True)) + + Sum("activities__unfunded_cash", filter=Q(activities__is_active=True)) ), ) return results["total"] if results["total"] is not None else 0 @@ -2955,6 +2956,7 @@ class InterventionBudget(TimeStampedModel): verbose_name=_('UNICEF Supplies') ) total = models.DecimalField(max_digits=20, decimal_places=2, verbose_name=_('Total')) + total_unfunded = models.DecimalField(max_digits=20, decimal_places=2, default=0, verbose_name=_('Total Unfunded')) # sum of all activity/management budget cso/partner values partner_contribution_local = models.DecimalField(max_digits=20, decimal_places=2, default=0, @@ -2977,6 +2979,10 @@ class InterventionBudget(TimeStampedModel): max_digits=20, decimal_places=2, default=0, verbose_name=_('Total HQ Cash Local') ) + unfunded_hq_cash = models.DecimalField( + max_digits=20, decimal_places=2, default=0, + verbose_name=_('Unfunded Capacity Strengthening Cash Local') + ) # unicef cash including headquarters contribution unicef_cash_local = models.DecimalField(max_digits=20, decimal_places=2, default=0, verbose_name=_('Unicef Cash Local')) @@ -2985,6 +2991,8 @@ class InterventionBudget(TimeStampedModel): max_digits=20, decimal_places=2, default=0, verbose_name=_('UNICEF Supplies Local') ) + has_unfunded_cash = models.BooleanField(verbose_name=_("Unfunded Cash"), default=False) + currency = CurrencyField(verbose_name=_('Currency'), null=False, default='') total_local = models.DecimalField(max_digits=20, decimal_places=2, verbose_name=_('Total Local')) programme_effectiveness = models.DecimalField( @@ -3016,7 +3024,7 @@ def total_unicef_contribution_local(self): return self.unicef_cash_local + self.in_kind_amount_local def total_cash_local(self): - return self.partner_contribution_local + self.unicef_cash_local + return self.partner_contribution_local + self.unicef_cash_local + self.total_unfunded @transaction.atomic def save(self, **kwargs): @@ -3049,6 +3057,7 @@ def calc_totals(self, save=True): def init_totals(): self.partner_contribution_local = 0 self.total_unicef_cash_local_wo_hq = 0 + self.total_unfunded = 0 init = False for link in intervention.result_links.all(): @@ -3059,6 +3068,7 @@ def init_totals(): init = True self.partner_contribution_local += activity.cso_cash self.total_unicef_cash_local_wo_hq += activity.unicef_cash + self.total_unfunded += activity.unfunded_cash programme_effectiveness = 0 if not init: @@ -3066,8 +3076,12 @@ def init_totals(): programme_effectiveness += intervention.management_budgets.total self.partner_contribution_local += intervention.management_budgets.partner_total self.total_unicef_cash_local_wo_hq += intervention.management_budgets.unicef_total + self.total_unfunded += intervention.management_budgets.unfunded_total self.unicef_cash_local = self.total_unicef_cash_local_wo_hq + self.total_hq_cash_local + # add Capacity Strenghtening Unfunded to total_unfunded + self.total_unfunded += self.unfunded_hq_cash + # in kind totals self.in_kind_amount_local = 0 self.partner_supply_local = 0 @@ -3077,9 +3091,9 @@ def init_totals(): else: self.partner_supply_local += item.total_price - self.total = self.total_unicef_contribution() + self.partner_contribution + self.total = self.total_unicef_contribution() + self.partner_contribution + self.total_unfunded self.total_partner_contribution_local = self.partner_contribution_local + self.partner_supply_local - self.total_local = self.total_unicef_contribution_local() + self.total_partner_contribution_local + self.total_local = self.total_unicef_contribution_local() + self.total_partner_contribution_local + self.total_unfunded if self.total_local: self.programme_effectiveness = programme_effectiveness / self.total_local * 100 else: @@ -3518,6 +3532,12 @@ class InterventionManagementBudget(TimeStampedModel): max_digits=20, default=0, ) + act1_unfunded = models.DecimalField( + verbose_name=_("Unfunded amount for In-country management and support staff prorated to their contribution to the programme (representation, planning, coordination, logistics, administration, finance)"), + decimal_places=2, + max_digits=20, + default=0, + ) act2_unicef = models.DecimalField( verbose_name=_("UNICEF contribution for Operational costs prorated to their contribution to the programme (office space, equipment, office supplies, maintenance)"), decimal_places=2, @@ -3530,6 +3550,12 @@ class InterventionManagementBudget(TimeStampedModel): max_digits=20, default=0, ) + act2_unfunded = models.DecimalField( + verbose_name=_("Unfunded amount for Operational costs prorated to their contribution to the programme (office space, equipment, office supplies, maintenance)"), + decimal_places=2, + max_digits=20, + default=0, + ) act3_unicef = models.DecimalField( verbose_name=_("UNICEF contribution for Planning, monitoring, evaluation and communication, prorated to their contribution to the programme (venue, travels, etc.)"), decimal_places=2, @@ -3542,6 +3568,12 @@ class InterventionManagementBudget(TimeStampedModel): max_digits=20, default=0, ) + act3_unfunded = models.DecimalField( + verbose_name=_("Unfunded amount for Planning, monitoring, evaluation and communication, prorated to their contribution to the programme (venue, travels, etc.)"), + decimal_places=2, + max_digits=20, + default=0, + ) @property def partner_total(self): @@ -3551,9 +3583,13 @@ def partner_total(self): def unicef_total(self): return self.act1_unicef + self.act2_unicef + self.act3_unicef + @property + def unfunded_total(self): + return self.act1_unfunded + self.act2_unfunded + self.act3_unfunded + @property def total(self): - return self.partner_total + self.unicef_total + return self.partner_total + self.unicef_total + self.unfunded_total def save(self, *args, **kwargs): create = not self.pk @@ -3565,17 +3601,22 @@ def save(self, *args, **kwargs): def update_cash(self): aggregated_items = self.items.values('kind').order_by('kind') - aggregated_items = aggregated_items.annotate(unicef_cash=Sum('unicef_cash'), cso_cash=Sum('cso_cash')) + aggregated_items = aggregated_items.annotate( + unicef_cash=Sum('unicef_cash'), cso_cash=Sum('cso_cash'), unfunded_cash=Sum('unfunded_cash') + ) for item in aggregated_items: if item['kind'] == InterventionManagementBudgetItem.KIND_CHOICES.in_country: self.act1_unicef = item['unicef_cash'] self.act1_partner = item['cso_cash'] + self.act1_unfunded = item['unfunded_cash'] elif item['kind'] == InterventionManagementBudgetItem.KIND_CHOICES.operational: self.act2_unicef = item['unicef_cash'] self.act2_partner = item['cso_cash'] + self.act2_unfunded = item['unfunded_cash'] elif item['kind'] == InterventionManagementBudgetItem.KIND_CHOICES.planning: self.act3_unicef = item['unicef_cash'] self.act3_partner = item['cso_cash'] + self.act3_unfunded = item['unfunded_cash'] self.save() @@ -3705,6 +3746,13 @@ class InterventionManagementBudgetItem(models.Model): default=0, ) + unfunded_cash = models.DecimalField( + verbose_name=_("Unfunded Cash Local"), + decimal_places=2, + max_digits=20, + default=0, + ) + class Meta: ordering = ('id',) diff --git a/src/etools/applications/partners/permission_matrix/intervention_permissions.csv b/src/etools/applications/partners/permission_matrix/intervention_permissions.csv index 286f81c4ca..69c6fc91e0 100644 --- a/src/etools/applications/partners/permission_matrix/intervention_permissions.csv +++ b/src/etools/applications/partners/permission_matrix/intervention_permissions.csv @@ -79,6 +79,8 @@ Field no,Field Name,Group,Condition,Status,Action,Allowed ,has_special_conditions_for_construction,Unicef Focal Point,,Active,Edit,TRUE ,has_special_conditions_for_construction,Unicef Focal Point,,Ended,Edit,TRUE ,has_special_conditions_for_construction,Unicef Focal Point,,Suspended,Edit,TRUE +,has_unfunded_cash,*,,*,View,TRUE +,has_unfunded_cash,Unicef Focal Point,,Draft,Edit,TRUE 3.4.1,contingency_pd,Unicef Focal Point,,Draft,Edit,TRUE 3.5,country_programme,Unicef Focal Point,,Draft,Edit,TRUE 3.5,country_programme,Unicef Focal Point,user_adds_amendment,*,Edit,TRUE diff --git a/src/etools/applications/partners/permissions.py b/src/etools/applications/partners/permissions.py index bcd7cb28cf..0ce8306b6a 100644 --- a/src/etools/applications/partners/permissions.py +++ b/src/etools/applications/partners/permissions.py @@ -99,7 +99,10 @@ def get_permissions(self): class InterventionPermissions(PMPPermissions): MODEL_NAME = 'partners.Intervention' - EXTRA_FIELDS = ['sections_present', 'pd_outputs', 'final_partnership_review', 'prc_reviews', 'document_currency'] + EXTRA_FIELDS = [ + 'sections_present', 'pd_outputs', 'final_partnership_review', 'prc_reviews', + 'document_currency', 'has_unfunded_cash' + ] def __init__(self, **kwargs): """ diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index 118f08816d..48183cb7e3 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -63,6 +63,8 @@ class InterventionBudgetCUSerializer( total_cash_local = serializers.DecimalField(max_digits=20, decimal_places=2) total_local = serializers.DecimalField(max_digits=20, decimal_places=2) total_supply = serializers.DecimalField(max_digits=20, decimal_places=2) + total_unfunded = serializers.DecimalField(max_digits=20, decimal_places=2) + unfunded_hq_cash = serializers.DecimalField(max_digits=20, decimal_places=2) class Meta: model = InterventionBudget @@ -82,7 +84,10 @@ class Meta: "total_cash_local", "total_unicef_cash_local_wo_hq", "total_hq_cash_local", - "total_supply" + "total_supply", + "total_unfunded", + "unfunded_hq_cash", + "has_unfunded_cash" ) read_only_fields = ( "total_local", @@ -91,9 +96,21 @@ class Meta: "total_unicef_cash_local_wo_hq", "partner_supply_local", "total_partner_contribution_local", - "total_supply" + "total_supply", + "total_unfunded" ) + def validate_has_unfunded_cash(self, value): + if not value and self.instance.total_unfunded: + raise serializers.ValidationError(_('This programme document has unfunded amounts. ' + 'Please fix them before deactivating.')) + return value + + def validate_unfunded_hq_cash(self, value): + if value and not self.instance.has_unfunded_cash: + raise serializers.ValidationError(_('This programme document does not include unfunded amounts.')) + return value + def get_intervention(self): return self.validated_data['intervention'] diff --git a/src/etools/applications/partners/serializers/interventions_v3.py b/src/etools/applications/partners/serializers/interventions_v3.py index 4e30919a87..4cfb17e6c9 100644 --- a/src/etools/applications/partners/serializers/interventions_v3.py +++ b/src/etools/applications/partners/serializers/interventions_v3.py @@ -149,7 +149,8 @@ def extract_file_data(self): class InterventionManagementBudgetItemSerializer(serializers.ModelSerializer): default_error_messages = { - 'invalid_budget': _('Invalid budget data. Total cash should be equal to items number * price per item.') + 'invalid_budget': _('Invalid budget data. Total cash should be equal to items number * price per item.'), + 'pd_is_funded': _('This programme document does not include unfunded amounts.') } id = serializers.IntegerField(required=False) @@ -159,7 +160,7 @@ class Meta: fields = ( 'id', 'kind', 'name', 'unit', 'unit_price', 'no_units', - 'unicef_cash', 'cso_cash' + 'unicef_cash', 'cso_cash', 'unfunded_cash' ) def validate(self, attrs): @@ -173,9 +174,13 @@ def validate(self, attrs): no_units = attrs.get('no_units', instance.no_units if instance else 0) unicef_cash = attrs.get('unicef_cash', instance.unicef_cash if instance else 0) cso_cash = attrs.get('cso_cash', instance.cso_cash if instance else 0) + unfunded_cash = attrs.get('unfunded_cash', self.instance.unfunded_cash if self.instance else 0) + + if unfunded_cash and not self.root.get_intervention().planned_budget.has_unfunded_cash: + self.fail('pd_is_funded') # unit_price * no_units can contain more decimal places than we're able to save - if abs((unit_price * no_units) - (unicef_cash + cso_cash)) > 0.01: + if abs((unit_price * no_units) - (unicef_cash + cso_cash + unfunded_cash)) > 0.01: self.fail('invalid_budget') return attrs @@ -191,6 +196,7 @@ class InterventionManagementBudgetSerializer( act3_total = serializers.SerializerMethodField() partner_total = serializers.DecimalField(max_digits=20, decimal_places=2, read_only=True) unicef_total = serializers.DecimalField(max_digits=20, decimal_places=2, read_only=True) + unfunded_total = serializers.DecimalField(max_digits=20, decimal_places=2, read_only=True) total = serializers.DecimalField(max_digits=20, decimal_places=2, read_only=True) class Meta: @@ -199,26 +205,30 @@ class Meta: "items", "act1_unicef", "act1_partner", + "act1_unfunded", "act1_total", "act2_unicef", "act2_partner", + "act2_unfunded", "act2_total", "act3_unicef", "act3_partner", + "act3_unfunded", "act3_total", "partner_total", "unicef_total", + "unfunded_total", "total", ) def get_act1_total(self, obj): - return str(obj.act1_unicef + obj.act1_partner) + return str(obj.act1_unicef + obj.act1_partner + obj.act1_unfunded) def get_act2_total(self, obj): - return str(obj.act2_unicef + obj.act2_partner) + return str(obj.act2_unicef + obj.act2_partner + obj.act2_unfunded) def get_act3_total(self, obj): - return str(obj.act3_unicef + obj.act3_partner) + return str(obj.act3_unicef + obj.act3_partner + obj.act3_unfunded) @transaction.atomic def update(self, instance, validated_data): @@ -240,6 +250,7 @@ def set_items(self, instance, items): ) else: serializer = InterventionManagementBudgetItemSerializer(data=item) + serializer.bind(field_name='root', parent=self) if not serializer.is_valid(): raise ValidationError({'items': {i: serializer.errors}}) diff --git a/src/etools/applications/partners/tests/test_amendments.py b/src/etools/applications/partners/tests/test_amendments.py index d8a74ef01f..ee6e29afb2 100644 --- a/src/etools/applications/partners/tests/test_amendments.py +++ b/src/etools/applications/partners/tests/test_amendments.py @@ -570,6 +570,23 @@ def test_update_difference_on_merge(self): self.assertIn('end', amendment.difference) self.assertIn('management_budgets', amendment.difference) + def test_update_difference_on_merge_unfunded_cash(self): + amendment = InterventionAmendmentFactory( + intervention=self.active_intervention, + kind=InterventionAmendment.KIND_NORMAL, + ) + + amendment.amended_intervention.management_budgets.act1_unfunded = Decimal("2.0") + amendment.amended_intervention.save() + + self.assertDictEqual(amendment.difference, {}) + + amendment.difference = amendment.get_difference() + amendment.merge_amendment() + + self.assertIn('management_budgets', amendment.difference) + self.assertEqual(amendment.difference['management_budgets']['diff']['act1_unfunded']['diff'], (0, '2.0')) + def test_update_intervention_risk(self): original_risk = InterventionRiskFactory(intervention=self.active_intervention) amendment = InterventionAmendmentFactory( diff --git a/src/etools/applications/partners/tests/test_api_amendments.py b/src/etools/applications/partners/tests/test_api_amendments.py index c309244f91..6f4aa378ea 100644 --- a/src/etools/applications/partners/tests/test_api_amendments.py +++ b/src/etools/applications/partners/tests/test_api_amendments.py @@ -23,12 +23,17 @@ AgreementFactory, InterventionAmendmentFactory, InterventionFactory, + InterventionResultLinkFactory, InterventionReviewFactory, InterventionSupplyItemFactory, PartnerFactory, ) +from etools.applications.reports.models import ResultType from etools.applications.reports.tests.factories import ( CountryProgrammeFactory, + InterventionActivityFactory, + InterventionActivityItemFactory, + LowerResultFactory, OfficeFactory, ReportingRequirementFactory, SectionFactory, @@ -595,3 +600,26 @@ def test_merge_error(self): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn('Merge Error', response.data[0]) + + def test_merge_unfunded_cash(self): + amended_budget = self.amended_intervention.planned_budget + amended_budget.has_unfunded_cash = True + amended_budget.unfunded_hq_cash = 13 + amended_budget.save() + + result_link = InterventionResultLinkFactory( + intervention=self.amended_intervention, + cp_output__result_type__name=ResultType.OUTPUT, + ) + pd_output = LowerResultFactory(result_link=result_link) + activity = InterventionActivityFactory(result=pd_output) + InterventionActivityItemFactory(activity=activity, unicef_cash=8) + InterventionActivityItemFactory(activity=activity, unicef_cash=8, unfunded_cash=10) + + response = self.forced_auth_req( + 'patch', + reverse('pmp_v3:intervention-amendment-merge', args=[self.amended_intervention.pk]), + self.unicef_focal_point, + data={} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) diff --git a/src/etools/applications/partners/tests/test_api_interventions.py b/src/etools/applications/partners/tests/test_api_interventions.py index a2932c89a0..9b0e6e5219 100644 --- a/src/etools/applications/partners/tests/test_api_interventions.py +++ b/src/etools/applications/partners/tests/test_api_interventions.py @@ -130,6 +130,7 @@ class TestInterventionsAPI(BaseTenantTestCase): "has_data_processing_agreement", "has_activities_involving_children", "has_special_conditions_for_construction", + "has_unfunded_cash", "hq_support_cost", "humanitarian_flag", "id", diff --git a/src/etools/applications/partners/tests/test_v3_intervention_activity.py b/src/etools/applications/partners/tests/test_v3_intervention_activity.py index 8c45e5f11b..197c725df1 100644 --- a/src/etools/applications/partners/tests/test_v3_intervention_activity.py +++ b/src/etools/applications/partners/tests/test_v3_intervention_activity.py @@ -76,6 +76,43 @@ def test_set_cash_values_directly(self): str(self.intervention.planned_budget.total_cash_local()), ) + def test_set_unfunded_cash_when_pd_funded(self): + self.assertFalse(self.intervention.planned_budget.has_unfunded_cash) + response = self.forced_auth_req( + 'patch', self.detail_url, + user=self.user, + data={ + 'unicef_cash': 1, + 'cso_cash': 2, + 'unfunded_cash': 1 + } + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_set_unfunded_cash_when_pd_unfunded(self): + self.intervention.planned_budget.has_unfunded_cash = True + self.intervention.planned_budget.save() + response = self.forced_auth_req( + 'patch', self.detail_url, + user=self.user, + data={ + 'unicef_cash': 1, + 'cso_cash': 2, + 'unfunded_cash': 1 + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(response.data['unicef_cash'], '1.00') + self.assertEqual(response.data['cso_cash'], '2.00') + self.assertEqual(response.data['unfunded_cash'], '1.00') + self.assertEqual(response.data['partner_percentage'], '50.00') + self.intervention.refresh_from_db() + budget_response = response.data["intervention"]["planned_budget"] + self.assertEqual( + budget_response["total_cash_local"], + str(self.intervention.planned_budget.total_cash_local()), + ) + def test_set_cash_values_from_items(self): InterventionActivityItemFactory(activity=self.activity, unicef_cash=8) response = self.forced_auth_req( @@ -106,6 +143,40 @@ def test_set_cash_values_from_items(self): self.assertEqual(response.data['cso_cash'], '6.20') self.assertEqual(response.data['partner_percentage'], '67.39') # cso_cash / (unicef_cash + cso_cash) + def test_set_cash_values_from_items_unfunded(self): + self.intervention.planned_budget.has_unfunded_cash = True + self.intervention.planned_budget.save() + InterventionActivityItemFactory(activity=self.activity, unicef_cash=8) + response = self.forced_auth_req( + 'patch', self.detail_url, + user=self.user, + data={ + 'items': [ + { + 'name': 'first_item', + 'unit': 'item', 'no_units': 1, 'unit_price': '8.0', + 'unicef_cash': '3.0', 'cso_cash': '4.0', 'unfunded_cash': '1.0' + }, + { + 'name': 'second_item', + 'unit': 'item', 'no_units': 1, 'unit_price': '2.0', + 'unicef_cash': '0.0', 'cso_cash': '1.0', 'unfunded_cash': '1.0' + }, + { + 'name': 'third_item', + 'unit': 'item', 'no_units': 1, 'unit_price': '2.0', + 'unicef_cash': '0.8', 'cso_cash': '0.2', 'unfunded_cash': '1.0' + } + ], + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(response.data['unicef_cash'], '3.80') + self.assertEqual(response.data['cso_cash'], '5.20') + self.assertEqual(response.data['partner_percentage'], '43.33') # cso_cash / (unicef_cash + cso_cash) + self.assertEqual(response.data['unfunded_cash'], '3.00') + self.assertEqual(response.data['intervention']['planned_budget']['total_unfunded'], '3.00') + def test_set_bad_cash_values_having_items(self): InterventionActivityItemFactory(activity=self.activity, unicef_cash=8, cso_cash=5) self.activity.update_cash() diff --git a/src/etools/applications/partners/tests/test_v3_intervention_results_structure.py b/src/etools/applications/partners/tests/test_v3_intervention_results_structure.py index 6b4e036cde..ccbabd7dd0 100644 --- a/src/etools/applications/partners/tests/test_v3_intervention_results_structure.py +++ b/src/etools/applications/partners/tests/test_v3_intervention_results_structure.py @@ -54,7 +54,7 @@ def test_retrieve(self): activity2 = InterventionActivityFactory(result=self.pd_output) InterventionActivityItemFactory(activity=activity2, unicef_cash=8) - InterventionActivityItemFactory(activity=activity2, unicef_cash=8) + InterventionActivityItemFactory(activity=activity2, unicef_cash=8, unfunded_cash=10) quarter.activities.add(activity2) response = self.forced_auth_req( @@ -66,8 +66,12 @@ def test_retrieve(self): links = response.data["result_links"][0] self.assertIn("total", links) + self.assertEqual(len(links["ll_results"]), 1) self.assertEqual(len(links["ll_results"][0]['activities']), 2) + # test unfunded_cash is added to result_links total and ll_results total + self.assertEqual(links["ll_results"][0]['total'], links['total']) + for actual_activity, expected_activity in zip(links["ll_results"][0]['activities'], [activity1, activity2]): self.assertEqual(actual_activity['id'], expected_activity.pk) @@ -77,14 +81,14 @@ def test_retrieve(self): expected_activity.created.strftime('%Y-%m-%dT%H:%M:%S.%fZ') ) for field in ['name', 'code', 'context_details', - 'unicef_cash', 'cso_cash']: + 'unicef_cash', 'cso_cash', 'unfunded_cash']: self.assertEqual(actual_activity[field], str(getattr(expected_activity, field))) self.assertEqual(len(actual_activity['items']), expected_activity.items.count()) for actual_item, expected_item in zip(actual_activity['items'], expected_activity.items.all()): for field in ['name', 'unit', 'unit_price', 'no_units', - 'unicef_cash', 'cso_cash']: + 'unicef_cash', 'cso_cash', 'unfunded_cash']: self.assertEqual(actual_item[field], str(getattr(expected_item, field))) def test_retrieve_no_result_activities(self): diff --git a/src/etools/applications/partners/tests/test_v3_interventions.py b/src/etools/applications/partners/tests/test_v3_interventions.py index f83647cf24..4c8919fdae 100644 --- a/src/etools/applications/partners/tests/test_v3_interventions.py +++ b/src/etools/applications/partners/tests/test_v3_interventions.py @@ -471,6 +471,37 @@ def test_cfei_number_permissions_country_office_admin(self): self.assertTrue(response.data['permissions']['view']['cfei_number']) self.assertTrue(response.data['permissions']['edit']['cfei_number']) + def test_unfunded_cash_totals_in_result_links(self): + result_link = self.intervention.result_links.first() + ll_result = result_link.ll_results.first() + InterventionActivityFactory(result=ll_result, unicef_cash=10, cso_cash=20, unfunded_cash=30) + response = self.forced_auth_req( + "get", + reverse('pmp_v3:intervention-detail', args=[self.intervention.pk]), + user=self.unicef_user, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + links = response.data["result_links"][0] + self.assertIn("total", links) + # test unfunded_cash is added to result_links total and ll_results total + self.assertEqual(links['total'], 90) + self.assertEqual(links['total'], links["ll_results"][0]['total']) + + def test_has_unfunded_cash_permissions_partner_user(self): + staff_member = UserFactory( + realms__data=['IP Viewer'], + profile__organization=self.intervention.agreement.partner.organization, + ) + self.intervention.partner_focal_points.add(staff_member) + response = self.forced_auth_req( + "get", + reverse('pmp_v3:intervention-detail', args=[self.intervention.pk]), + user=staff_member, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data['permissions']['view']['has_unfunded_cash']) + self.assertFalse(response.data['permissions']['edit']['has_unfunded_cash']) + def test_pdf_partner_user(self): staff_member = UserFactory( realms__data=['IP Viewer'], @@ -817,6 +848,116 @@ def test_patch_currency(self): budget.refresh_from_db() self.assertEqual(budget.currency, "PEN") + def test_patch_activate_has_unfunded_cash_unicef_focal_point(self): + intervention = InterventionFactory() + intervention.unicef_focal_points.add(self.unicef_user) + budget = intervention.planned_budget + self.assertFalse(budget.has_unfunded_cash) + + response = self.forced_auth_req( + "patch", + reverse('pmp_v3:intervention-detail', args=[intervention.pk]), + user=self.unicef_user, + data={'planned_budget': { + "id": budget.pk, + "has_unfunded_cash": True, + }} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + budget.refresh_from_db() + self.assertTrue(budget.has_unfunded_cash) + + def test_patch_activate_has_unfunded_cash_partner_user(self): + intervention = InterventionFactory(date_sent_to_partner=datetime.date.today()) + staff_member = UserFactory( + realms__data=['IP Viewer'], + profile__organization=intervention.agreement.partner.organization, + ) + intervention.partner_focal_points.add(staff_member) + budget = intervention.planned_budget + self.assertFalse(budget.has_unfunded_cash) + + response = self.forced_auth_req( + "patch", + reverse('pmp_v3:intervention-detail', args=[intervention.pk]), + user=staff_member, + data={'planned_budget': { + "id": budget.pk, + "has_unfunded_cash": True + }} + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, ["Cannot change fields while in draft: planned_budget"]) + + def test_patch_deactivate_has_unfunded_cash(self): + intervention = InterventionFactory() + intervention.unicef_focal_points.add(self.unicef_user) + budget = intervention.planned_budget + budget.has_unfunded_cash = True + budget.save() + self.forced_auth_req( + "patch", + reverse('pmp_v3:intervention-detail', args=[intervention.pk]), + user=self.unicef_user, + data={'planned_budget': { + "id": budget.pk, + "unfunded_hq_cash": 1234, + }} + ) + budget.refresh_from_db() + self.assertEqual(budget.unfunded_hq_cash, 1234) + response = self.forced_auth_req( + "patch", + reverse('pmp_v3:intervention-detail', args=[intervention.pk]), + user=self.unicef_user, + data={'planned_budget': { + "id": budget.pk, + "has_unfunded_cash": False, + }} + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'This programme document has unfunded amounts. ' + 'Please fix them before deactivating.', + response.data['planned_budget']['has_unfunded_cash'] + ) + + def test_patch_unfunded_hq_cash_unicef_focal_point(self): + intervention = InterventionFactory() + intervention.unicef_focal_points.add(self.unicef_user) + budget = intervention.planned_budget + self.assertEqual(budget.unfunded_hq_cash, 0) + + response = self.forced_auth_req( + "patch", + reverse('pmp_v3:intervention-detail', args=[intervention.pk]), + user=self.unicef_user, + data={'planned_budget': { + "id": budget.pk, + "unfunded_hq_cash": 1234, + }} + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'This programme document does not include unfunded amounts.', + response.data['planned_budget']['unfunded_hq_cash'] + ) + budget.has_unfunded_cash = True + budget.save(update_fields=['has_unfunded_cash']) + + response = self.forced_auth_req( + "patch", + reverse('pmp_v3:intervention-detail', args=[intervention.pk]), + user=self.unicef_user, + data={'planned_budget': { + "id": budget.pk, + "unfunded_hq_cash": 1234, + }} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + budget.refresh_from_db() + self.assertEqual(budget.unfunded_hq_cash, 1234) + def test_patch_country_programme(self): intervention = InterventionFactory() agreement = intervention.agreement @@ -1181,6 +1322,98 @@ def test_set_cash_values_from_items(self): self.assertEqual(response.data['act3_unicef'], '0.00') self.assertEqual(response.data['act3_partner'], '2.00') + def test_set_cash_values_from_items_unfunded(self): + intervention = InterventionFactory() + intervention.planned_budget.has_unfunded_cash = True + intervention.planned_budget.save() + + InterventionManagementBudgetItemFactory(budget=intervention.management_budgets, unicef_cash=8) + response = self.forced_auth_req( + 'patch', + reverse( + "pmp_v3:intervention-budget", + args=[intervention.pk], + ), + user=self.unicef_user, + data={ + 'act1_unicef': 1, + 'act1_partner': 2, + 'act2_unicef': 3, + 'act2_partner': 4, + 'items': [ + { + 'name': 'first_item', 'kind': 'operational', + 'unit': 'item', 'no_units': '1.0', 'unit_price': '7.0', + 'unicef_cash': '3.0', 'cso_cash': '1.5', 'unfunded_cash': '2.5' + }, + { + 'name': 'second_item', 'kind': 'planning', + 'unit': 'item', 'no_units': '1.0', 'unit_price': '2.0', + 'unicef_cash': '0.0', 'cso_cash': '2.0', + }, + { + 'name': 'third_item', 'kind': 'operational', + 'unit': 'item', 'no_units': '1.0', 'unit_price': '0.2', + 'unicef_cash': '0.0', 'cso_cash': '0.2', + } + ], + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(response.data['act1_unicef'], '1.00') + self.assertEqual(response.data['act1_partner'], '2.00') + self.assertEqual(response.data['act1_unfunded'], '0.00') + self.assertEqual(response.data['act2_unicef'], '3.00') + self.assertEqual(response.data['act2_partner'], '1.70') + self.assertEqual(response.data['act2_unfunded'], '2.50') + self.assertEqual(response.data['act3_unicef'], '0.00') + self.assertEqual(response.data['act3_partner'], '2.00') + self.assertEqual(response.data['act3_unfunded'], '0.00') + + def test_update_items_unfunded_cash(self): + intervention = InterventionFactory() + item_to_update = InterventionManagementBudgetItemFactory( + budget=intervention.management_budgets, + kind='planning', + no_units=1, unit_price=42, + unicef_cash=20, cso_cash=20, + unfunded_cash=2 + ) + self.assertEqual(intervention.management_budgets.items.count(), 1) + intervention.planned_budget.has_unfunded_cash = True + intervention.planned_budget.save() + response = self.forced_auth_req( + 'patch', + reverse( + "pmp_v3:intervention-budget", + args=[intervention.pk], + ), + user=self.unicef_user, + data={ + 'items': [ + {'id': item_to_update.id, 'unit_price': '44', 'unfunded_cash': '4'}, + { + 'name': 'first_item', 'kind': 'operational', + 'unit': 'test', 'no_units': '1.0', 'unit_price': '6.0', + 'unicef_cash': '1.0', 'cso_cash': '2.0', 'unfunded_cash': '3.0' + } + ], + } + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(intervention.management_budgets.items.count(), 2) + self.assertEqual(len(response.data['items']), 2) + self.assertEqual(response.data['act1_unicef'], '0.00') + self.assertEqual(response.data['act1_partner'], '0.00') + self.assertEqual(response.data['act1_unfunded'], '0.00') + self.assertEqual(response.data['act2_unicef'], '1.00') + self.assertEqual(response.data['act2_partner'], '2.00') + self.assertEqual(response.data['act2_unfunded'], '3.00') + self.assertEqual(response.data['act3_unicef'], '20.00') + self.assertEqual(response.data['act3_partner'], '20.00') + self.assertEqual(response.data['act3_unfunded'], '4.00') + def test_set_items(self): intervention = InterventionFactory() item_to_remove = InterventionManagementBudgetItemFactory( @@ -1269,7 +1502,7 @@ def test_budget_validation(self): response.data['items'][0]['non_field_errors'], ) - def test_budget_item_validation_rouding_ok(self): + def test_budget_item_validation_rounding_ok(self): intervention = InterventionFactory() item_to_update = InterventionManagementBudgetItemFactory(budget=intervention.management_budgets) diff --git a/src/etools/applications/reports/migrations/0046_auto_20230328_0930.py b/src/etools/applications/reports/migrations/0046_auto_20230328_0930.py new file mode 100644 index 0000000000..ad6beaa1f8 --- /dev/null +++ b/src/etools/applications/reports/migrations/0046_auto_20230328_0930.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2023-03-28 09:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0045_lowerresult_is_active'), + ] + + operations = [ + migrations.AddField( + model_name='interventionactivity', + name='unfunded_cash', + field=models.DecimalField(decimal_places=2, default=0, max_digits=20, verbose_name='Unfunded Cash'), + ), + migrations.AddField( + model_name='interventionactivityitem', + name='unfunded_cash', + field=models.DecimalField(decimal_places=2, default=0, max_digits=20, verbose_name='Unfunded Cash'), + ), + ] diff --git a/src/etools/applications/reports/models.py b/src/etools/applications/reports/models.py index 82e286e684..8ea930c437 100644 --- a/src/etools/applications/reports/models.py +++ b/src/etools/applications/reports/models.py @@ -415,7 +415,9 @@ def renumber_results_for_result_link(cls, result_link): def total(self): results = self.activities.aggregate( - total=Sum("unicef_cash", filter=Q(is_active=True)) + Sum("cso_cash", filter=Q(is_active=True)), + total=Sum("unicef_cash", filter=Q(is_active=True)) + + Sum("cso_cash", filter=Q(is_active=True)) + + Sum("unfunded_cash", filter=Q(is_active=True)) ) return results["total"] if results["total"] is not None else 0 @@ -431,6 +433,12 @@ def total_unicef(self): ) return results["total"] if results["total"] is not None else 0 + def total_unfunded(self): + results = self.activities.aggregate( + total=Sum("unfunded_cash", filter=Q(is_active=True)), + ) + return results["total"] if results["total"] is not None else 0 + class Unit(models.Model): """ @@ -1036,6 +1044,12 @@ class InterventionActivity(TimeStampedModel): max_digits=20, default=0, ) + unfunded_cash = models.DecimalField( + verbose_name=_("Unfunded Cash"), + decimal_places=2, + max_digits=20, + default=0, + ) time_frames = models.ManyToManyField( 'InterventionTimeFrame', @@ -1063,14 +1077,16 @@ def update_cash(self): aggregates = items.aggregate( unicef_cash=Sum('unicef_cash'), cso_cash=Sum('cso_cash'), + unfunded_cash=Sum('unfunded_cash'), ) self.unicef_cash = aggregates['unicef_cash'] self.cso_cash = aggregates['cso_cash'] + self.unfunded_cash = aggregates['unfunded_cash'] self.save() @property def total(self): - return self.unicef_cash + self.cso_cash + return self.unicef_cash + self.cso_cash + self.unfunded_cash @property def partner_percentage(self): @@ -1099,7 +1115,8 @@ def renumber_activities_for_result(cls, result: LowerResult, start_id=None): cls.objects.bulk_update(activities, fields=['code']) def get_amended_name(self): - return f'{self.result} {self.name} (Total: {self.total}, UNICEF: {self.unicef_cash}, Partner: {self.cso_cash})' + return f'{self.result} {self.name} (Total: {self.total}, ' \ + f'UNICEF: {self.unicef_cash} | Unfunded: {self.unfunded_cash}, Partner: {self.cso_cash})' def get_time_frames_display(self): return ', '.join([f'{tf.start_date.year} Q{tf.quarter}' for tf in self.time_frames.all()]) @@ -1149,6 +1166,12 @@ class InterventionActivityItem(TimeStampedModel): max_digits=20, default=0, ) + unfunded_cash = models.DecimalField( + verbose_name=_("Unfunded Cash"), + decimal_places=2, + max_digits=20, + default=0, + ) class Meta: verbose_name = _('Intervention Activity Item') diff --git a/src/etools/applications/reports/serializers/v2.py b/src/etools/applications/reports/serializers/v2.py index afc77fb10e..6696ede59f 100644 --- a/src/etools/applications/reports/serializers/v2.py +++ b/src/etools/applications/reports/serializers/v2.py @@ -520,7 +520,8 @@ class Meta: class InterventionActivityItemSerializer(serializers.ModelSerializer): default_error_messages = { - 'invalid_budget': _('Invalid budget data. Total cash should be equal to items number * price per item.') + 'invalid_budget': _('Invalid budget data. Total cash should be equal to items number * price per item.'), + 'pd_is_funded': _('This programme document does not include unfunded amounts.') } id = serializers.IntegerField(required=False) @@ -536,6 +537,7 @@ class Meta: 'no_units', 'unicef_cash', 'cso_cash', + 'unfunded_cash' ) def validate(self, attrs): @@ -545,9 +547,13 @@ def validate(self, attrs): no_units = attrs.get('no_units', self.instance.no_units if self.instance else 0) unicef_cash = attrs.get('unicef_cash', self.instance.unicef_cash if self.instance else 0) cso_cash = attrs.get('cso_cash', self.instance.cso_cash if self.instance else 0) + unfunded_cash = attrs.get('unfunded_cash', self.instance.unfunded_cash if self.instance else 0) + + if unfunded_cash and not self.root.intervention.planned_budget.has_unfunded_cash: + self.fail('pd_is_funded') # unit_price * no_units can contain more decimal places than we're able to save - if abs((unit_price * no_units) - (unicef_cash + cso_cash)) > 0.01: + if abs((unit_price * no_units) - (unicef_cash + cso_cash + unfunded_cash)) > 0.01: self.fail('invalid_budget') return attrs @@ -664,6 +670,7 @@ class Meta: 'context_details', 'unicef_cash', 'cso_cash', + 'unfunded_cash', 'items', 'time_frames', 'partner_percentage', @@ -675,6 +682,11 @@ def __init__(self, *args, **kwargs): self.intervention = kwargs.pop('intervention', None) super().__init__(*args, **kwargs) + def validate_unfunded_cash(self, value): + if value and not self.intervention.planned_budget.has_unfunded_cash: + raise serializers.ValidationError(_('This programme document does not include unfunded amounts.')) + return value + def validate(self, attrs): attrs = super().validate(attrs) if self.instance and self.partial and 'items' not in attrs and self.instance.items.exists(): @@ -682,6 +694,7 @@ def validate(self, attrs): # it's easy to break total values, so we ignore them attrs.pop('unicef_cash', None) attrs.pop('cso_cash', None) + attrs.pop('unfunded_cash', None) return attrs @transaction.atomic @@ -734,7 +747,7 @@ class Meta: model = InterventionActivity fields = ( 'id', 'name', 'code', 'context_details', - 'unicef_cash', 'cso_cash', 'partner_percentage', + 'unicef_cash', 'cso_cash', 'unfunded_cash', 'partner_percentage', 'time_frames', 'is_active', 'created', ) read_only_fields = ['code']