Skip to main content
Early Access - The Dcycle CLI is currently available for enterprise customers. Contact us to learn more about access.

Multi-Organization Consolidated Reporting

For holding companies and enterprise groups, this guide shows how to automate consolidated sustainability reporting across multiple organizations.

Use Case

Your company structure:
Acme Holding Corp
├── Acme Spain (manufacturing)
├── Acme France (manufacturing)
├── Acme UK (sales)
└── Acme Logistics (transport)
You need to:
  • Generate a single consolidated report
  • Compare performance across subsidiaries
  • Track group-wide reduction targets

Quick Start: Export All Organizations

#!/bin/bash
# export_all_orgs.sh - Export data from all organizations

YEAR=${1:-2024}
OUTPUT_DIR="reports/$YEAR"
mkdir -p "$OUTPUT_DIR"

echo "📊 Exporting data for all organizations ($YEAR)..."

# Get list of all organizations
ORG_IDS=$(dc org list --format json | jq -r '.[].id')

for org_id in $ORG_IDS; do
    # Get organization name (sanitized for filename)
    org_name=$(dc org show $org_id --format json | jq -r '.name' | tr ' /' '_')

    echo "  Exporting: $org_name"

    # Set organization context
    dc org set $org_id

    # Export key data
    dc facility list --format json > "$OUTPUT_DIR/${org_name}_facilities.json"
    dc vehicle list --format json > "$OUTPUT_DIR/${org_name}_vehicles.json"

    echo "    ✓ Exported to $OUTPUT_DIR/${org_name}_*.json"
done

echo "✅ Export complete: $OUTPUT_DIR/"

Consolidated Emissions Report

Generate a single report with emissions from all subsidiaries:
#!/bin/bash
# consolidated_emissions.sh

YEAR=${1:-2024}
OUTPUT_FILE="reports/consolidated_emissions_$YEAR.json"

echo "📊 Generating consolidated emissions report for $YEAR..."

# Start JSON array
echo "[" > "$OUTPUT_FILE"
first=true

for org_id in $(dc org list --format json | jq -r '.[].id'); do
    # Set context
    dc org set $org_id > /dev/null

    # Get org details
    org_info=$(dc org show $org_id --format json)
    org_name=$(echo $org_info | jq -r '.name')
    org_country=$(echo $org_info | jq -r '.country')

    # Get emissions (assuming this command exists)
    # Adapt to actual CLI commands available
    emissions=$(dc emissions summary --year $YEAR --format json 2>/dev/null || echo '{}')

    # Add comma separator
    if [ "$first" = true ]; then
        first=false
    else
        echo "," >> "$OUTPUT_FILE"
    fi

    # Write organization data
    cat >> "$OUTPUT_FILE" << EOF
  {
    "organization_id": "$org_id",
    "name": "$org_name",
    "country": "$org_country",
    "year": $YEAR,
    "emissions": $emissions
  }
EOF

    echo "  ✓ $org_name"
done

# Close JSON array
echo "]" >> "$OUTPUT_FILE"

echo "✅ Report saved to: $OUTPUT_FILE"

Comparison Report

Compare performance across subsidiaries:
#!/bin/bash
# compare_subsidiaries.sh

YEAR=${1:-2024}

echo "═══════════════════════════════════════════════════════════════════"
echo "  SUSTAINABILITY PERFORMANCE BY SUBSIDIARY - $YEAR"
echo "═══════════════════════════════════════════════════════════════════"
echo ""
printf "%-25s %10s %10s %10s %12s\n" "Organization" "Scope 1" "Scope 2" "Scope 3" "Total"
printf "%-25s %10s %10s %10s %12s\n" "────────────" "───────" "───────" "───────" "─────"

TOTAL_S1=0
TOTAL_S2=0
TOTAL_S3=0
TOTAL_ALL=0

for org_id in $(dc org list --format json | jq -r '.[].id'); do
    dc org set $org_id > /dev/null

    name=$(dc org show $org_id --format json | jq -r '.name' | cut -c1-25)

    # Get emissions by scope
    # Adapt these commands to your actual CLI
    scope1=$(dc vehicle list --format json | jq '[.[].emissions_tco2e // 0] | add // 0')
    scope2=$(dc invoice list --year $YEAR --format json 2>/dev/null | jq '[.[].emissions_tco2e // 0] | add // 0' || echo "0")
    scope3=$(dc purchase list --year $YEAR --format json 2>/dev/null | jq '[.[].emissions_tco2e // 0] | add // 0' || echo "0")

    total=$(echo "$scope1 + $scope2 + $scope3" | bc)

    printf "%-25s %10.1f %10.1f %10.1f %12.1f\n" "$name" "$scope1" "$scope2" "$scope3" "$total"

    TOTAL_S1=$(echo "$TOTAL_S1 + $scope1" | bc)
    TOTAL_S2=$(echo "$TOTAL_S2 + $scope2" | bc)
    TOTAL_S3=$(echo "$TOTAL_S3 + $scope3" | bc)
    TOTAL_ALL=$(echo "$TOTAL_ALL + $total" | bc)
done

echo "────────────────────────────────────────────────────────────────────"
printf "%-25s %10.1f %10.1f %10.1f %12.1f\n" "GROUP TOTAL" "$TOTAL_S1" "$TOTAL_S2" "$TOTAL_S3" "$TOTAL_ALL"
echo ""
echo "All values in tCO2e"
Output:
═══════════════════════════════════════════════════════════════════
  SUSTAINABILITY PERFORMANCE BY SUBSIDIARY - 2024
═══════════════════════════════════════════════════════════════════

Organization              Scope 1    Scope 2    Scope 3       Total
────────────              ───────    ───────    ───────       ─────
Acme Spain                  456.2      234.5      678.9      1369.6
Acme France                 234.1      189.3      456.7       880.1
Acme UK                      98.4      123.4      234.5       456.3
Acme Logistics              987.6      145.2      567.8      1700.6
────────────────────────────────────────────────────────────────────
GROUP TOTAL                1776.3      692.4     1937.9      4406.6

All values in tCO2e

Python Consolidation Script

For more complex reporting needs:
#!/usr/bin/env python3
# consolidate_report.py

import json
import subprocess
from dataclasses import dataclass
from typing import Optional

@dataclass
class OrgEmissions:
    org_id: str
    name: str
    country: str
    scope_1: float = 0
    scope_2: float = 0
    scope_3: float = 0

    @property
    def total(self) -> float:
        return self.scope_1 + self.scope_2 + self.scope_3

def run_dc(*args) -> dict:
    """Run dc command and return JSON output"""
    result = subprocess.run(
        ['dc'] + list(args) + ['--format', 'json'],
        capture_output=True,
        text=True
    )
    if result.returncode != 0:
        return {}
    return json.loads(result.stdout) if result.stdout else {}

def get_all_organizations() -> list[dict]:
    """Get list of all accessible organizations"""
    return run_dc('org', 'list')

def set_organization(org_id: str):
    """Set active organization context"""
    subprocess.run(['dc', 'org', 'set', org_id], capture_output=True)

def get_org_emissions(org_id: str, year: int) -> OrgEmissions:
    """Get emissions for a single organization"""
    set_organization(org_id)

    # Get org info
    org_info = run_dc('org', 'show', org_id)

    emissions = OrgEmissions(
        org_id=org_id,
        name=org_info.get('name', 'Unknown'),
        country=org_info.get('country', '??')
    )

    # Get scope 1 (vehicles)
    vehicles = run_dc('vehicle', 'list')
    if isinstance(vehicles, list):
        emissions.scope_1 = sum(v.get('emissions_tco2e', 0) for v in vehicles)

    # Get scope 2 (invoices) - adapt to actual commands
    # emissions.scope_2 = ...

    # Get scope 3 (purchases, logistics) - adapt to actual commands
    # emissions.scope_3 = ...

    return emissions

def generate_consolidated_report(year: int) -> dict:
    """Generate consolidated emissions report"""
    orgs = get_all_organizations()
    all_emissions = []

    for org in orgs:
        emissions = get_org_emissions(org['id'], year)
        all_emissions.append(emissions)

    # Calculate totals
    total_s1 = sum(e.scope_1 for e in all_emissions)
    total_s2 = sum(e.scope_2 for e in all_emissions)
    total_s3 = sum(e.scope_3 for e in all_emissions)

    return {
        'year': year,
        'generated_at': datetime.now().isoformat(),
        'organizations': [
            {
                'id': e.org_id,
                'name': e.name,
                'country': e.country,
                'scope_1_tco2e': e.scope_1,
                'scope_2_tco2e': e.scope_2,
                'scope_3_tco2e': e.scope_3,
                'total_tco2e': e.total,
            }
            for e in all_emissions
        ],
        'group_totals': {
            'scope_1_tco2e': total_s1,
            'scope_2_tco2e': total_s2,
            'scope_3_tco2e': total_s3,
            'total_tco2e': total_s1 + total_s2 + total_s3,
        }
    }

if __name__ == '__main__':
    import sys
    from datetime import datetime

    year = int(sys.argv[1]) if len(sys.argv) > 1 else 2024
    report = generate_consolidated_report(year)
    print(json.dumps(report, indent=2))

Year-over-Year Tracking

Track progress against reduction targets:
#!/bin/bash
# yoy_tracking.sh

CURRENT_YEAR=2024
BASELINE_YEAR=2020
TARGET_REDUCTION=50  # percent by 2030

echo "═══════════════════════════════════════════════════════════════════"
echo "  YEAR-OVER-YEAR PROGRESS TRACKING"
echo "  Baseline: $BASELINE_YEAR | Current: $CURRENT_YEAR | Target: -${TARGET_REDUCTION}% by 2030"
echo "═══════════════════════════════════════════════════════════════════"
echo ""

printf "%-20s %12s %12s %10s %10s\n" "Organization" "$BASELINE_YEAR" "$CURRENT_YEAR" "Change" "On Track?"
printf "%-20s %12s %12s %10s %10s\n" "────────────" "────────" "────────" "──────" "─────────"

for org_id in $(dc org list --format json | jq -r '.[].id'); do
    dc org set $org_id > /dev/null

    name=$(dc org show $org_id --format json | jq -r '.name' | cut -c1-20)

    # Get emissions for both years (adapt to actual commands)
    baseline=1000  # placeholder - replace with actual query
    current=800    # placeholder - replace with actual query

    if [ "$baseline" -gt 0 ]; then
        change=$(echo "scale=1; (($current - $baseline) / $baseline) * 100" | bc)
        # Required rate to hit target by 2030
        years_remaining=$((2030 - CURRENT_YEAR))
        years_elapsed=$((CURRENT_YEAR - BASELINE_YEAR))
        required_rate=$(echo "scale=1; $TARGET_REDUCTION / (2030 - $BASELINE_YEAR) * $years_elapsed" | bc)
        actual_reduction=$(echo "scale=1; -1 * $change" | bc)

        if (( $(echo "$actual_reduction >= $required_rate" | bc -l) )); then
            status="✅ Yes"
        else
            status="⚠️  Behind"
        fi

        printf "%-20s %12.1f %12.1f %9.1f%% %10s\n" "$name" "$baseline" "$current" "$change" "$status"
    fi
done

Automated Monthly Report

Schedule a monthly consolidated report:
# .github/workflows/monthly-consolidated-report.yml
name: Monthly Consolidated Report

on:
  schedule:
    - cron: '0 7 1 * *'  # 1st of each month at 7 AM

jobs:
  generate-report:
    runs-on: ubuntu-latest
    env:
      DCYCLE_API_KEY: ${{ secrets.DCYCLE_API_KEY }}

    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: pip install dcycle-cli

      - name: Generate consolidated report
        run: |
          python scripts/consolidate_report.py $(date +%Y) > report.json

      - name: Upload report artifact
        uses: actions/upload-artifact@v4
        with:
          name: consolidated-report-${{ github.run_number }}
          path: report.json

      - name: Send to stakeholders
        run: |
          # Send via email, Slack, or upload to shared drive
          curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H 'Content-Type: application/json' \
            -d @- << EOF
          {
            "text": "📊 Monthly consolidated sustainability report is ready",
            "blocks": [
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "*Monthly Consolidated Report*\nGenerated: $(date)\nDownload from: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                }
              }
            ]
          }
          EOF

Best Practices

Consistent Data Periods

Ensure all subsidiaries report for the same time period to enable accurate consolidation.

Currency Normalization

If using spend-based factors, normalize currencies before consolidating.

Boundary Definitions

Document which entities are included/excluded from consolidated reports.

Intercompany Elimination

Avoid double-counting internal logistics or shared services.

Next Steps