Repository: akvo/akvo-rsr

Issue:
Bug:  IATI transaction errors links missing 
### What were you doing?

The IATI file overview needs to allow the user to quickly link to the 'transaction' that as an error: 

![image-23](https://user-images.githubusercontent.com/26227538/163149195-18d5e4a2-93e8-4b8b-8a65-9fe39b1cba4c.png)



### What should've happened?

A direct link to transaction in the Project Editor (section 6) that has reported an error, similar to other error messages. 

### My environment

Safari / MacBook Pro 2017

### Additional context

_No response_


Discussion:

Resolving patch:
diff --git a/akvo/iati/checks/fields/budgets.py b/akvo/iati/checks/fields/budgets.py
--- a/akvo/iati/checks/fields/budgets.py
+++ b/akvo/iati/checks/fields/budgets.py
@@ -4,6 +4,8 @@
 # See more details in the license.txt file located at the root folder of the Akvo RSR module.
 # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
 
+import json
+
 
 def budgets(project):
     """
@@ -20,33 +22,38 @@ def budgets(project):
     for budget in project.budget_items.all():
         if budget.amount is None:
             all_checks_passed = False
-            checks.append(('error', 'budget (id: %s) has no amount' % str(budget.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'budget', 'id': budget.id, 'message': f'budget (id: {budget.id}) has no amount'})))
 
         if not budget.period_start:
             all_checks_passed = False
-            checks.append(('error', 'budget (id: %s) has no start date' % str(budget.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'budget', 'id': budget.id, 'message': f'budget (id: {budget.id}) has no start date'})))
 
         if not budget.period_end:
             all_checks_passed = False
-            checks.append(('error', 'budget (id: %s) has no end date' % str(budget.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'budget', 'id': budget.id, 'message': f'budget (id: {budget.id}) has no end date'})))
 
         if budget.period_start and budget.period_end and budget.period_start > budget.period_end:
             all_checks_passed = False
-            checks.append(('error', 'budget (id: %s) has a start date before the end date' %
-                           str(budget.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'budget', 'id': budget.id, 'message': f'budget (id: {budget.id}) has a start date before end date'})))
 
         if budget.period_start and budget.period_end and (budget.period_end - budget.period_start).days > 365:
             all_checks_passed = False
-            checks.append(('error', f"budget (id: {budget.pk}) period must not be longer than one year"))
+            checks.append(('error', json.dumps({
+                'model': 'budget', 'id': budget.id, 'message': f'budget (id: {budget.id}) period must not be longer than one year'})))
 
         if not budget.currency and not project.currency:
             all_checks_passed = False
-            checks.append(('error', 'budget (id: %s) has no currency and no default currency '
-                           'specified' % str(budget.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'budget', 'id': budget.id, 'message': f'budget (id: {budget.id}) has no currency and no default currency specified'})))
 
         if not budget.value_date:
             all_checks_passed = False
-            checks.append(('error', 'budget (id: %s) has no value date' % str(budget.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'budget', 'id': budget.id, 'message': f'budget (id: {budget.id}) has no value date'})))
 
     if project.budget_items.all() and all_checks_passed:
         checks.append(('success', 'has valid budget items'))
diff --git a/akvo/iati/checks/fields/transactions.py b/akvo/iati/checks/fields/transactions.py
--- a/akvo/iati/checks/fields/transactions.py
+++ b/akvo/iati/checks/fields/transactions.py
@@ -4,6 +4,8 @@
 # See more details in the license.txt file located at the root folder of the Akvo RSR module.
 # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
 
+import json
+
 from datetime import date
 
 
@@ -26,50 +28,53 @@ def transactions(project):
     for transaction in project.transactions.prefetch_related(*prefetch_attributes).all():
         if not transaction.transaction_type:
             all_checks_passed = False
-            checks.append(('error', 'transaction (id: %s) has no type' % str(transaction.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'transaction (id: {transaction.id}) has no type'})))
 
         if not transaction.transaction_date:
             all_checks_passed = False
-            checks.append(('error', 'transaction (id: %s) has no date' % str(transaction.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'transaction (id: {transaction.id}) has no date'})))
         elif transaction.transaction_date > date.today():
             all_checks_passed = False
-            checks.append(('error', 'transaction (id: %s) has a date in the future' %
-                           str(transaction.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'transaction (id: {transaction.id}) has a date in the future'})))
 
         if transaction.value is None:
             all_checks_passed = False
-            checks.append(('error', 'transaction (id: %s) has no value' % str(transaction.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'transaction (id: {transaction.id}) has no value'})))
 
         if not transaction.value_date:
             all_checks_passed = False
-            checks.append(('error', 'transaction (id: %s) has no value date' %
-                           str(transaction.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'transaction (id: {transaction.id}) has no value date'})))
 
         if not (transaction.currency or project.currency):
             all_checks_passed = False
-            checks.append(('error', 'transaction (id: %s) has no currency and no default '
-                           'currency specified' % str(transaction.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'transaction (id: {transaction.id}) has no currency and no default currency specified'})))
 
         if transaction.receiver_organisation and not transaction.receiver_organisation.iati_org_id:
-            checks.append(('warning', 'receiver organisation of transaction (id: %s) has no '
-                           'IATI identifier' % str(transaction.pk)))
+            checks.append(('warning', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'receiver organisation of transaction (id: {transaction.id}) has no IATI identifier'})))
 
         if transaction.provider_organisation and not transaction.provider_organisation.iati_org_id:
-            checks.append(('warning', 'provider organisation of transaction (id: %s) has no '
-                           'IATI identifier' % str(transaction.pk)))
+            checks.append(('warning', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'provider organisation of transaction (id: {transaction.id}) has no IATI identifier'})))
 
         if (transaction.recipient_region_vocabulary
                 or transaction.recipient_region_vocabulary_uri) and not transaction.recipient_region:
             all_checks_passed = False
-            checks.append(('error', 'transaction (id: %s) is missing a recipient region' %
-                           str(transaction.pk)))
-
-        if transaction.recipient_region_vocabulary == '99' and \
-                not transaction.recipient_region_vocabulary_uri:
-            checks.append(('warning', 'transaction (id: %s) recipient region has vocabulary 99 '
-                           '(reporting organisation), but no vocabulary URI '
-                           'specified' %
-                           str(transaction.pk)))
+            checks.append(('error', json.dumps({
+                'model': 'transaction', 'id': transaction.id, 'message': f'transaction (id: {transaction.id}) is missing a recipient region'})))
+
+        if transaction.recipient_region_vocabulary == '99' and not transaction.recipient_region_vocabulary_uri:
+            checks.append(('warning', json.dumps({
+                'model': 'transaction',
+                'id': transaction.id,
+                'message': f'transaction (id: {transaction.id}) recipient region has vocabulary 99 (reporting organisation), but no vocabulary URI specified'
+            })))
 
     if project.transactions.all() and all_checks_passed:
         checks.append(('success', 'has valid transaction(s)'))
diff --git a/akvo/rsr/management/commands/perform_iati_checks.py b/akvo/rsr/management/commands/perform_iati_checks.py
--- a/akvo/rsr/management/commands/perform_iati_checks.py
+++ b/akvo/rsr/management/commands/perform_iati_checks.py
@@ -5,8 +5,9 @@
 # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
 import argparse
 import datetime
+import sys
 
-from akvo.rsr.models.project import Project
+from akvo.rsr.models import Project, Organisation
 
 from django.core.management.base import BaseCommand
 
@@ -36,28 +37,49 @@ def add_arguments(self, parser: argparse.ArgumentParser):
             type=date_arg_type,
             help='Limit to projects created on or before this day',
         )
+        parser.add_argument(
+            '-o', '--org-id',
+            type=int,
+            help='Check all project of an organisation. Other options will be ignored if this option is set',
+        )
 
     def handle(self, *args, **options):
+        projects = self.get_projects(options)
+
+        self.stdout.write('Performing IATI checks for {} ...'.format(projects.count()))
+        for project in projects:
+            self.stdout.write('Performing IATI checks for project {0}...'.format(project.pk))
+            project.update_iati_checks()
+
+    def get_projects(self, options):
+        org_id = options["org_id"]
+        if org_id:
+            try:
+                org = Organisation.objects.get(id=org_id)
+                return org.all_projects()
+            except Organisation.DoesNotExist:
+                self.stderr.write("Organisation not found")
+                sys.exit(1)
+
         all_option = options["all"]
         date_start = options["date_start"]
         date_end = options["date_end"]
 
-        # Filter projects with options
         projects = Project.objects.all()
+
         if not (all_option or date_start or date_end):
             self.stdout.write("No options provided: only checking projects with run_iati_checks=True")
-            projects = projects.filter(run_iati_checks=True)
-        elif all_option:
+            return projects.filter(run_iati_checks=True)
+
+        if all_option:
             self.stdout.write("Checking ALL projects. This might take a while...")
-        else:
-            if date_start:
-                self.stdout.write("Filtering projects on and after %s" % date_start)
-                projects = projects.filter(created_at__gte=date_start)
-            if date_end:
-                self.stdout.write("Filtering projects on and before %s" % date_end)
-                projects = projects.filter(created_at__lte=date_end)
+            return projects
 
-        self.stdout.write('Performing IATI checks for {} ...'.format(projects.count()))
-        for project in projects:
-            self.stdout.write('Performing IATI checks for project {0}...'.format(project.pk))
-            project.update_iati_checks()
+        if date_start:
+            self.stdout.write("Filtering projects on and after %s" % date_start)
+            projects = projects.filter(created_at__gte=date_start)
+        if date_end:
+            self.stdout.write("Filtering projects on and before %s" % date_end)
+            projects = projects.filter(created_at__lte=date_end)
+
+        return projects


Test patch: