Procure-to-Pay Financial Audit Dashboard

Build a P2P audit pipeline with Python, detect anomalies, and visualize in Power BI.

Introduction

30 Second Summary

Every time a company pays an invoice twice, misses a purchase order, or sends payment late, money leaks out of the business. Finance teams at large enterprises lose millions each year to these seemingly small errors. Catching them requires digging through thousands of transaction records.

In this project, you will build a complete Procure-to-Pay financial audit simulation. You will generate a realistic 10,000+ row mock dataset with Python, detect procurement anomalies programmatically, and present your findings through an interactive Power BI dashboard and executive summary.

What You'll Build

A portfolio-ready financial audit project that simulates what an analytics engineer would deliver to a finance operations team.

By the end of this project, you'll have:

  • A Python data generator that produces two related CSV tables (Vendors, Invoice Logs) with 10,000+ rows containing realistic Procure-to-Pay anomalies like duplicate invoices, missing purchase orders, and late payment penalties.
  • A multi-page Power BI dashboard with a Process Health Index gauge, Leakage Summary KPI cards, and a drill-through vendor risk matrix with conditional formatting.
  • A 5-slide executive summary presentation that frames your data findings as a business case and proposes automated PO-matching rules to reduce invoice exceptions.
  • Secret Mission: Add a Vendor Similarity Score using Python's fuzzy matching to catch near-duplicate vendor names and surface them in a dedicated Power BI page.

Are there any prerequisites?

You need basic Python familiarity (variables, loops, functions) and a basic understanding of spreadsheets. No prior Power BI or finance domain experience is required.

Power BI Desktop is Windows-only. If you are on macOS, you will need a Windows virtual machine or access to a Windows computer for the dashboard steps.

Before We Start

To build a financial audit that uncovers duplicate payments and late penalties, you need a few tools. Python generates the mock ERP data, and Power BI turns that data into an interactive dashboard for finance stakeholders.

In this step, you will install Python with the required packages, set up Power BI Desktop, and create the project folder structure that organizes your audit deliverables.

In this step, get ready to:
  • Install Python 3.11+ with pandas and Faker for data generation.
  • Install Power BI Desktop for dashboard visualization.
  • Create the project folder structure with subfolders for data, reports, and presentation.
Install Python and required packages

This project requires Python 3.11 or higher. Let's check whether you already have it installed.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Python version by running this command:

macOS

python3 --version

Windows

python --version

✔️ I see version 3.11 or higher

You are all set. Move on to the package installation below.

ⓧ I see an older version

Your system has a Python version below 3.11. On macOS, the system-installed Python is often an older version that shadows newer installs.

  • Go to python.org/downloads and download the latest Python 3 installer for your operating system.
  • Run the installer. On macOS, double-click the .pkg file and follow the prompts. On Windows, run the .exe and check the box that says Add python.exe to PATH before clicking Install Now.
  • Close and reopen your terminal, then run the version check again. On macOS, try python3.11 --version if python3 --version still shows the old version.

ⓧ Command not found

Python is not installed on your machine yet.

  • Go to python.org/downloads and download the latest Python 3 installer for your operating system.
  • Run the installer. On macOS, double-click the .pkg file and follow the prompts. On Windows, run the .exe and check the box that says Add python.exe to PATH before clicking Install Now.
  • Close and reopen your terminal, then run the version check again to confirm it shows 3.11 or higher.

Now install the two Python packages this project uses: pandas for data manipulation and Faker for generating realistic mock data.

  • Install pandas and Faker by running this command:

macOS

pip3 install pandas==2.2.3 faker==40.12.0

Windows

pip install pandas==2.2.3 faker==40.12.0
  • Verify both packages installed correctly by running:

macOS

python3 -c "import pandas; import faker; print('Ready')"

Windows

python -c "import pandas; import faker; print('Ready')"

You should see Ready printed in your terminal. This confirms both packages are available.

Not seeing Ready?

If you get a ModuleNotFoundError, your pip and python commands might point to different Python installations. Try this on macOS to ensure packages install to the correct Python:

python3 -m pip install pandas==2.2.3 faker==40.12.0

Help me fix my pandas and Faker installation

Install Power BI Desktop

Power BI Desktop is the free visualization tool you will use to build the audit dashboard. It is a Windows-only application.

What if I'm on macOS?

Power BI Desktop requires Windows 10 or Windows 11. If you are on macOS, you can use a Windows virtual machine (Parallels, UTM, or Boot Camp) or access Power BI through the browser-based Power BI Service with a free Microsoft account.

  • Open the Microsoft Store on your Windows computer. Press the Windows key, type Microsoft Store, and press Enter.
  • Search for Power BI Desktop in the Store's search bar.
  • Click Install (or Get) and wait for the download to complete.
  • Once installed, click Open to launch Power BI Desktop. You should see the Power BI splash screen followed by the Home view.

Why the Microsoft Store?

Installing from the Microsoft Store gives you automatic updates and does not require admin privileges. You can also download the installer directly from the Microsoft Download Center if you prefer.

Power BI not launching?

Make sure you are running Windows 10 or later (64-bit). If Power BI Desktop opens to a blank screen, check that WebView2 is installed on your system. You can download it from Microsoft's WebView2 page.

Help me get Power BI Desktop working

Create the project folder structure

Your audit project needs a clean folder structure to keep the generated data, Power BI reports, and executive presentation organized.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and subfolders by running these commands:
mkdir p2p-audit
cd p2p-audit
mkdir data reports presentation

What are these folders for?

  • data/ stores the CSV files your Python script generates.
  • reports/ holds your Power BI .pbix dashboard file.
  • presentation/ is where your executive summary slides will live.
  • Confirm the folders exist by running:
ls

You should see data, reports, and presentation listed in the output.

Don't see all three folders?

Make sure you ran the mkdir command from inside the p2p-audit folder (after the cd p2p-audit command). If you are in the wrong directory, the folders will be created elsewhere.

Help me fix my folder structure

Your environment is ready. Next up, you will write a Python script that generates 10,000+ rows of realistic invoice data with intentional anomalies baked in.

Generate Mock ERP Dataset with Python

Your environment is ready. Now it is time to create the dataset that powers the entire audit.

Real ERP systems contain thousands of invoice records with messy, imperfect data. In this step, you will write a Python script that generates a realistic 10,000+ row accounts payable dataset complete with intentionally injected anomalies like duplicate invoices, missing purchase orders, and late payments.

In this step, get ready to:
  • Build a 150-vendor master table with categories, payment terms, and risk tiers.
  • Generate 10,500 invoice records with realistic date ranges and dollar amounts.
  • Inject three types of P2P anomalies into the dataset and export the flagged results.
Build the vendor master table

Every invoice in an ERP system is linked to a vendor record. You will start by generating 150 unique vendors with attributes that mirror a real vendor master.

  • Create a new file called generate_data.py inside your p2p-audit folder by running:
  • In your terminal, make sure you are inside the p2p-audit folder first.
touch generate_data.py
  • Open generate_data.py in your code editor and add the following code to set up imports, configuration, and the vendor generation logic:
import pandas as pd
from faker import Faker
import random
import numpy as np
from datetime import timedelta

fake = Faker()
Faker.seed(42)
random.seed(42)
np.random.seed(42)

# --- Configuration ---
NUM_VENDORS = 150
NUM_INVOICES = 10500
DATE_START = pd.Timestamp("2023-01-01")
DATE_END = pd.Timestamp("2025-12-31")

VENDOR_CATEGORIES = ["Services", "Materials", "IT", "Logistics", "Consulting"]
PAYMENT_TERMS = ["Net-30", "Net-45", "Net-60"]
RISK_TIERS = ["Low", "Medium", "High"]

# --- Generate Vendors Table ---
vendors = []
for i in range(1, NUM_VENDORS + 1):
    vendors.append({
        "VendorID": f"V{i:04d}",
        "VendorName": fake.company(),
        "VendorCategory": random.choice(VENDOR_CATEGORIES),
        "PaymentTerms": random.choice(PAYMENT_TERMS),
        "RiskTier": random.choices(RISK_TIERS, weights=[50, 35, 15])[0],
    })

vendors_df = pd.DataFrame(vendors)
vendors_df.to_csv("data/vendors.csv", index=False)
print(f"Generated {len(vendors_df)} vendors.")

What does this code do?

The top section imports pandas for data manipulation, Faker for generating realistic company names, and numpy for statistical distributions. All random seeds are set to 42 so you get reproducible results every run.

The vendor loop creates 150 records, each with a category (Services, Materials, IT, Logistics, Consulting), payment terms (Net-30, Net-45, Net-60), and a weighted risk tier. The weights [50, 35, 15] mean most vendors are Low risk, fewer are Medium, and only 15% are High risk.

  • Save generate_data.py.
  • Run the script to verify the vendor table generates correctly:
python generate_data.py
  • You should see Generated 150 vendors. printed in your terminal.
  • Confirm the file was created by running:
ls data/
  • You should see vendors.csv listed in the output.

Seeing a ModuleNotFoundError?

If you see an error like ModuleNotFoundError: No module named 'pandas', your packages may not be installed in the active Python environment. Re-run pip install pandas==2.2.3 faker==40.12.0 and try again.

If you see a FileNotFoundError about the data/ folder, make sure you are running the script from inside the p2p-audit directory (not a parent folder).

help me fix this error when running generate_data.py

Build the invoice logs table

Each invoice record needs to reference a vendor, carry a realistic dollar amount, and include dates for goods receipt and payment. You will generate 10,500 base invoice records spanning three years of transaction history.

  • In generate_data.py, add the following code below the line print(f"Generated {len(vendors_df)} vendors."):
# --- Generate Base Invoice Logs ---
def get_payment_days(term):
    mapping = {"Net-30": 30, "Net-45": 45, "Net-60": 60}
    return mapping.get(term, 30)

invoices = []
for i in range(1, NUM_INVOICES + 1):
    vendor = random.choice(vendors)
    invoice_date = fake.date_between(start_date=DATE_START, end_date=DATE_END)
    invoice_date = pd.Timestamp(invoice_date)
    amount = round(np.random.lognormal(mean=8.5, sigma=1.2), 2)
    amount = max(500, min(amount, 150000))

    payment_term_days = get_payment_days(vendor["PaymentTerms"])
    due_date = invoice_date + timedelta(days=payment_term_days)
    payment_date = due_date + timedelta(days=random.randint(-10, 5))

    goods_receipt_date = invoice_date + timedelta(days=random.randint(1, 14))

    invoices.append({
        "InvoiceNumber": f"INV-{i:06d}",
        "VendorID": vendor["VendorID"],
        "VendorName": vendor["VendorName"],
        "InvoiceDate": invoice_date.strftime("%Y-%m-%d"),
        "Amount": amount,
        "PONumber": f"PO-{random.randint(10000, 99999)}",
        "GoodsReceiptDate": goods_receipt_date.strftime("%Y-%m-%d"),
        "PaymentDate": payment_date.strftime("%Y-%m-%d"),
        "PaymentTerms": vendor["PaymentTerms"],
        "Currency": "USD",
        "PenaltyAmount": 0.0,
    })

invoices_df = pd.DataFrame(invoices)

What does this code do?

The get_payment_days() helper converts text terms like Net-30 into numeric day counts. The loop then generates 10,500 invoices, each randomly assigned to one of the 150 vendors.

Invoice amounts use a log-normal distribution (np.random.lognormal), which produces values clustered around a central range with occasional large outliers. This mirrors real-world spending patterns where most invoices are moderate but a few are very large. The max(500, min(amount, 150000)) clamp keeps amounts between $500 and $150,000.

  • Save generate_data.py.

You will not see new output yet. The next substep adds the anomaly injection and final export that produces the completed dataset.

Inject P2P anomalies systematically

A clean dataset is useless for an audit. You need realistic defects to detect. The script will now inject three types of Procure-to-Pay anomalies that mirror real ERP problems: duplicate invoices with misspelled vendor names, invoices missing a purchase order (maverick spend), and payments made past the contractual deadline.

  • In generate_data.py, add the following code below the line invoices_df = pd.DataFrame(invoices). This section creates duplicate invoices with subtle vendor name variations:
# --- Inject Anomalies ---

# 1. Duplicate Invoices (~5%)
num_duplicates = int(NUM_INVOICES * 0.05)
duplicate_indices = random.sample(range(len(invoices_df)), num_duplicates)
duplicates = invoices_df.iloc[duplicate_indices].copy()

def misspell_vendor(name):
    variations = [
        lambda n: n + ".",
        lambda n: n.upper(),
        lambda n: n.replace(" ", "  "),
        lambda n: n.replace("&", "and"),
        lambda n: n.replace(",", ""),
        lambda n: n[:-1] if len(n) > 3 else n + " Inc",
    ]
    return random.choice(variations)(name)

duplicates["VendorName"] = duplicates["VendorName"].apply(misspell_vendor)
duplicates["InvoiceNumber"] = [
    f"INV-{NUM_INVOICES + j:06d}" for j in range(1, num_duplicates + 1)
]
duplicates["InvoiceDate"] = duplicates["InvoiceDate"].apply(
    lambda d: (pd.Timestamp(d) + timedelta(days=random.randint(1, 5))).strftime("%Y-%m-%d")
)
duplicates["Flag_DuplicateInvoice"] = True

Why misspell vendor names?

In real ERP systems, duplicate invoices often slip through because the vendor name is slightly different. A human might type "Acme Corp." one day and "ACME Corp" the next. The misspell_vendor() function simulates these variations by adding periods, changing case, doubling spaces, or swapping punctuation.

The duplicate copies also get new invoice numbers and slightly shifted dates, making them harder to catch with simple exact-match checks.

  • Continue adding the remaining anomaly injection, flag columns, summary metrics, and final export below the duplicates["Flag_DuplicateInvoice"] = True line:
# 2. Missing PO Invoices (~8%)
num_no_po = int(NUM_INVOICES * 0.08)
no_po_indices = random.sample(range(len(invoices_df)), num_no_po)
invoices_df.loc[no_po_indices, "PONumber"] = ""

# 3. Late Payments (~12%)
num_late = int(NUM_INVOICES * 0.12)
late_indices = random.sample(range(len(invoices_df)), num_late)
for idx in late_indices:
    row = invoices_df.loc[idx]
    payment_term_days = get_payment_days(row["PaymentTerms"])
    invoice_date = pd.Timestamp(row["InvoiceDate"])
    days_late = random.randint(15, 90)
    new_payment_date = invoice_date + timedelta(days=payment_term_days + days_late)
    invoices_df.loc[idx, "PaymentDate"] = new_payment_date.strftime("%Y-%m-%d")
    penalty = round(row["Amount"] * 0.015 * (days_late / 30), 2)
    invoices_df.loc[idx, "PenaltyAmount"] = penalty

# Add flag columns to base invoices
invoices_df["Flag_DuplicateInvoice"] = False
invoices_df["Flag_NoPO"] = invoices_df["PONumber"] == ""
invoices_df["Flag_LatePayment"] = invoices_df["PenaltyAmount"] > 0

# Combine base invoices with duplicates
duplicates["Flag_NoPO"] = False
duplicates["Flag_LatePayment"] = False
if "PenaltyAmount" not in duplicates.columns:
    duplicates["PenaltyAmount"] = 0.0

final_df = pd.concat([invoices_df, duplicates], ignore_index=True)

# --- Calculate Summary Metrics ---
total_invoices = len(final_df)
flagged_invoices = final_df[
    (final_df["Flag_DuplicateInvoice"]) |
    (final_df["Flag_NoPO"]) |
    (final_df["Flag_LatePayment"])
].shape[0]
process_health_index = round((1 - flagged_invoices / total_invoices) * 100, 1)
total_leakage = (
    final_df[final_df["Flag_DuplicateInvoice"]]["Amount"].sum()
    + final_df["PenaltyAmount"].sum()
)

print(f"\nGenerated {total_invoices} total invoice records.")
print(f"Flagged invoices: {flagged_invoices}")
print(f"Process Health Index: {process_health_index}%")
print(f"Total Leakage: ${total_leakage:,.2f}")

# Save final dataset
final_df.to_csv("data/invoice_logs_flagged.csv", index=False)
print("\nExported: data/invoice_logs_flagged.csv")

What does this code do?

This section injects two more anomaly types. Missing PO invoices (~8%) simulate maverick spend where someone bypasses the purchase order process. Late payments (~12%) push the payment date past the contractual deadline and calculate a penalty at 1.5% of the invoice amount per 30 days late.

The flag columns (Flag_DuplicateInvoice, Flag_NoPO, Flag_LatePayment) mark each anomaly type so Power BI can filter and visualize them later. The summary metrics give you an instant health check: a Process Health Index percentage and a Total Leakage dollar figure.

  • Save generate_data.py.
  • Run the full script by running:
python generate_data.py
  • You should see output similar to this in your terminal:
Generated 150 vendors.

Generated 11025 total invoice records.
Flagged invoices: 2521
Process Health Index: 77.1%
Total Leakage: $3,245,872.14

Exported: data/invoice_logs_flagged.csv

Why do the numbers differ slightly?

Your exact dollar amounts and flag counts depend on the random seed interaction with your system. The total invoice count should be around 11,025 (10,500 base + 525 duplicates). The Process Health Index should be roughly in the 75-80% range. These numbers confirm the anomaly injection is working.

  • Confirm both CSV files exist by running:
ls data/
  • You should see both vendors.csv and invoice_logs_flagged.csv listed.

Script runs but no CSV files appear?

Make sure you are running the script from the p2p-audit directory, not from inside the data/ subfolder. The script writes to relative paths like data/vendors.csv, so it expects the data/ folder to exist one level below where you run it.

If you see a KeyError or TypeError, check that you pasted the code sections in order. The anomaly injection section depends on invoices_df existing from the invoice generation section above it.

help me debug my generate_data.py script

✔️ Awesome, I've got everything!

Great. Make sure you have saved generate_data.py and confirmed both CSV files in your data/ folder.

ⓧ I'd like to double check the full code

Here is the complete generate_data.py file. Compare it with yours to spot any differences:

import pandas as pd
from faker import Faker
import random
import numpy as np
from datetime import timedelta

fake = Faker()
Faker.seed(42)
random.seed(42)
np.random.seed(42)

# --- Configuration ---
NUM_VENDORS = 150
NUM_INVOICES = 10500
DATE_START = pd.Timestamp("2023-01-01")
DATE_END = pd.Timestamp("2025-12-31")

VENDOR_CATEGORIES = ["Services", "Materials", "IT", "Logistics", "Consulting"]
PAYMENT_TERMS = ["Net-30", "Net-45", "Net-60"]
RISK_TIERS = ["Low", "Medium", "High"]

# --- Generate Vendors Table ---
vendors = []
for i in range(1, NUM_VENDORS + 1):
    vendors.append({
        "VendorID": f"V{i:04d}",
        "VendorName": fake.company(),
        "VendorCategory": random.choice(VENDOR_CATEGORIES),
        "PaymentTerms": random.choice(PAYMENT_TERMS),
        "RiskTier": random.choices(RISK_TIERS, weights=[50, 35, 15])[0],
    })

vendors_df = pd.DataFrame(vendors)
vendors_df.to_csv("data/vendors.csv", index=False)
print(f"Generated {len(vendors_df)} vendors.")

# --- Generate Base Invoice Logs ---
def get_payment_days(term):
    mapping = {"Net-30": 30, "Net-45": 45, "Net-60": 60}
    return mapping.get(term, 30)

invoices = []
for i in range(1, NUM_INVOICES + 1):
    vendor = random.choice(vendors)
    invoice_date = fake.date_between(start_date=DATE_START, end_date=DATE_END)
    invoice_date = pd.Timestamp(invoice_date)
    amount = round(np.random.lognormal(mean=8.5, sigma=1.2), 2)
    amount = max(500, min(amount, 150000))

    payment_term_days = get_payment_days(vendor["PaymentTerms"])
    due_date = invoice_date + timedelta(days=payment_term_days)
    payment_date = due_date + timedelta(days=random.randint(-10, 5))

    goods_receipt_date = invoice_date + timedelta(days=random.randint(1, 14))

    invoices.append({
        "InvoiceNumber": f"INV-{i:06d}",
        "VendorID": vendor["VendorID"],
        "VendorName": vendor["VendorName"],
        "InvoiceDate": invoice_date.strftime("%Y-%m-%d"),
        "Amount": amount,
        "PONumber": f"PO-{random.randint(10000, 99999)}",
        "GoodsReceiptDate": goods_receipt_date.strftime("%Y-%m-%d"),
        "PaymentDate": payment_date.strftime("%Y-%m-%d"),
        "PaymentTerms": vendor["PaymentTerms"],
        "Currency": "USD",
        "PenaltyAmount": 0.0,
    })

invoices_df = pd.DataFrame(invoices)

# --- Inject Anomalies ---

# 1. Duplicate Invoices (~5%)
num_duplicates = int(NUM_INVOICES * 0.05)
duplicate_indices = random.sample(range(len(invoices_df)), num_duplicates)
duplicates = invoices_df.iloc[duplicate_indices].copy()

def misspell_vendor(name):
    variations = [
        lambda n: n + ".",
        lambda n: n.upper(),
        lambda n: n.replace(" ", "  "),
        lambda n: n.replace("&", "and"),
        lambda n: n.replace(",", ""),
        lambda n: n[:-1] if len(n) > 3 else n + " Inc",
    ]
    return random.choice(variations)(name)

duplicates["VendorName"] = duplicates["VendorName"].apply(misspell_vendor)
duplicates["InvoiceNumber"] = [
    f"INV-{NUM_INVOICES + j:06d}" for j in range(1, num_duplicates + 1)
]
duplicates["InvoiceDate"] = duplicates["InvoiceDate"].apply(
    lambda d: (pd.Timestamp(d) + timedelta(days=random.randint(1, 5))).strftime("%Y-%m-%d")
)
duplicates["Flag_DuplicateInvoice"] = True

# 2. Missing PO Invoices (~8%)
num_no_po = int(NUM_INVOICES * 0.08)
no_po_indices = random.sample(range(len(invoices_df)), num_no_po)
invoices_df.loc[no_po_indices, "PONumber"] = ""

# 3. Late Payments (~12%)
num_late = int(NUM_INVOICES * 0.12)
late_indices = random.sample(range(len(invoices_df)), num_late)
for idx in late_indices:
    row = invoices_df.loc[idx]
    payment_term_days = get_payment_days(row["PaymentTerms"])
    invoice_date = pd.Timestamp(row["InvoiceDate"])
    days_late = random.randint(15, 90)
    new_payment_date = invoice_date + timedelta(days=payment_term_days + days_late)
    invoices_df.loc[idx, "PaymentDate"] = new_payment_date.strftime("%Y-%m-%d")
    penalty = round(row["Amount"] * 0.015 * (days_late / 30), 2)
    invoices_df.loc[idx, "PenaltyAmount"] = penalty

# Add flag columns to base invoices
invoices_df["Flag_DuplicateInvoice"] = False
invoices_df["Flag_NoPO"] = invoices_df["PONumber"] == ""
invoices_df["Flag_LatePayment"] = invoices_df["PenaltyAmount"] > 0

# Combine base invoices with duplicates
duplicates["Flag_NoPO"] = False
duplicates["Flag_LatePayment"] = False
if "PenaltyAmount" not in duplicates.columns:
    duplicates["PenaltyAmount"] = 0.0

final_df = pd.concat([invoices_df, duplicates], ignore_index=True)

# --- Calculate Summary Metrics ---
total_invoices = len(final_df)
flagged_invoices = final_df[
    (final_df["Flag_DuplicateInvoice"]) |
    (final_df["Flag_NoPO"]) |
    (final_df["Flag_LatePayment"])
].shape[0]
process_health_index = round((1 - flagged_invoices / total_invoices) * 100, 1)
total_leakage = (
    final_df[final_df["Flag_DuplicateInvoice"]]["Amount"].sum()
    + final_df["PenaltyAmount"].sum()
)

print(f"\nGenerated {total_invoices} total invoice records.")
print(f"Flagged invoices: {flagged_invoices}")
print(f"Process Health Index: {process_health_index}%")
print(f"Total Leakage: ${total_leakage:,.2f}")

# Save final dataset
final_df.to_csv("data/invoice_logs_flagged.csv", index=False)
print("\nExported: data/invoice_logs_flagged.csv")

Your mock ERP dataset is ready. You now have over 10,000 invoice records with realistic anomalies baked in. Next up, you will import this data into Power BI and build a data model that transforms these raw numbers into interactive audit visuals.

Build the Power BI Data Model

Your Python scripts have generated over 10,000 invoice records with realistic procurement anomalies. Now it is time to bring that data into Power BI Desktop and build a proper data model that connects your tables and calculates the key metrics your dashboard needs.

A data model in Power BI defines how your tables relate to each other and what calculations are available for visuals. In this step, you will import both CSVs, link them with a relationship on VendorID, create a dedicated Date table for time-based analysis, and write DAX measures that quantify leakage and process health.

In this step, get ready to:
  • Import your CSV datasets and create table relationships.
  • Build a calculated Date table and connect it to your invoice data.
  • Write DAX measures for leakage, penalties, and process health.
Import data into Power BI

Power BI needs to ingest your CSV files before you can build any visuals. You will load both tables and then connect them on their shared VendorID column.

  • Switch to Power BI Desktop (which you installed in Step 1).
  • Save your new blank report immediately: click File then Save As.
  • Navigate to the p2p-audit/reports/ folder and save the file as P2PAudit.pbix.
  • Import the invoice logs by clicking Get data on the Home tab.
  • Select Text/CSV from the list.
  • Browse to p2p-audit/data/invoice_logs_flagged.csv and click Open.
  • In the preview window, verify you see columns like InvoiceNumber, VendorID, Amount, and the Flag columns.
  • Click Load to import the data directly (no transformation needed).
  • Repeat the import for your vendor master: click Get data then Text/CSV again.
  • Browse to p2p-audit/data/vendors.csv and click Open.
  • Click Load to import.

What did Get Data just do?

Power BI created two tables in your data model: one named invoice_logs_flagged and one named vendors. Each table holds a copy of the CSV data in memory, ready for relationships and calculations.

Now you need to rename the tables to friendlier names and connect them.

  • Switch to Model view by clicking the model icon in the left sidebar (it looks like three connected boxes).
  • Double-click the invoice_logs_flagged table header and rename it to InvoiceLogs.
  • Double-click the vendors table header and rename it to Vendors.
  • Drag the VendorID field from the InvoiceLogs table onto the VendorID field on the Vendors table.
  • A line appears between the two tables, confirming the relationship is created.

Why create a relationship?

Relationships let Power BI combine data from multiple tables in a single visual. When you drop VendorCategory (from Vendors) into a chart alongside Amount (from InvoiceLogs), Power BI uses the VendorID relationship to match the right rows together.

  • Save your file by pressing Ctrl+S.

Don't see both tables in Model view?

Make sure you clicked Load (not Transform Data) in the preview window. If you accidentally opened Power Query Editor, click Close & Apply on the Home tab to push the data into your model.

Help me troubleshoot my missing tables

Create a Date table

Power BI time intelligence functions (like year-over-year comparisons) require a dedicated Date table with a continuous range of dates. You will create one using the DAX CALENDAR function.

  • In Model view, click New table in the Calculations group on the Home tab.
  • Enter this DAX formula in the formula bar:
DateTable = CALENDAR(DATE(2023,1,1), DATE(2025,12,31))
  • Press Enter to confirm. A new DateTable appears in the Fields pane with a single Date column.

What does CALENDAR do?

The CALENDAR function generates a table containing one row for every single date between the start and end dates you provide. Your table will have 1,096 rows (all dates from January 1, 2023 through December 31, 2025).

Next, add Year, Month, and Quarter columns so you can slice your dashboard by time periods.

  • Switch to Table view (the grid icon in the left sidebar).
  • Select the DateTable in the Fields pane on the right.
  • Click New column in the Table tools tab and enter:
Year = YEAR(DateTable[Date])
  • Press Enter.
  • Click New column again and enter:
Month = MONTH(DateTable[Date])
  • Press Enter.
  • Click New column one more time and enter:
Quarter = QUARTER(DateTable[Date])
  • Press Enter. Your DateTable now has four columns: Date, Year, Month, and Quarter.

Now mark it as the official Date table and relate it to your invoice dates.

  • With DateTable still selected, click Mark as date table in the Table tools tab.
  • Select the Date column as the date column and click OK.
  • Switch back to Model view.
  • Drag Date from the DateTable onto InvoiceDate in the InvoiceLogs table to create the relationship.
  • Save your file with Ctrl+S.

Why mark it as a Date table?

Marking a table as a Date table tells Power BI to use it for time intelligence features like year-to-date, quarter-over-quarter, and rolling averages. Without this flag, DAX time intelligence functions will not work correctly.

Getting an error when creating the relationship?

Power BI requires that the Date column in DateTable has no duplicate values and no blanks. If you see a "many-to-many" warning, confirm you dragged from the DateTable's Date column (which has unique values) to InvoiceLogs[InvoiceDate] (which has duplicates). The relationship direction should be one-to-many.

Help me fix this relationship issue

Create core DAX measures

Measures are dynamic calculations that respond to filters in your visuals. Unlike columns, measures are not stored row by row. They compute on the fly when a slicer, filter, or visual context changes.

  • In the Fields pane, right-click the InvoiceLogs table and select New measure.
  • Enter the first measure in the formula bar:
Total Invoices = COUNTROWS(InvoiceLogs)
  • Press Enter to confirm.
  • Right-click InvoiceLogs again and select New measure. Enter:
Total Invoice Amount = SUM(InvoiceLogs[Amount])
  • Press Enter.

What do these measures do?

Total Invoices counts every row in the InvoiceLogs table. Total Invoice Amount sums the Amount column. Both react to any filter applied in your report, so if you filter to Q1 2024, the measure only counts invoices from that quarter.

  • Create a third measure for duplicate payment amounts:
Duplicate Payment Amount = CALCULATE(SUM(InvoiceLogs[Amount]), InvoiceLogs[Flag_DuplicateInvoice] = TRUE())
  • Press Enter.
  • Create the late payment penalties measure:
Late Payment Penalties = SUM(InvoiceLogs[PenaltyAmount])
  • Press Enter.
  • Create the Total Leakage measure that combines both sources of capital loss:
Total Leakage = [Duplicate Payment Amount] + [Late Payment Penalties]
  • Press Enter.

What does CALCULATE do?

CALCULATE evaluates an expression (like SUM) while applying a filter you specify. In Duplicate Payment Amount, it sums only the rows where Flag_DuplicateInvoice is TRUE. This gives you the total dollar value of suspected duplicate payments.

Total Leakage references other measures using square brackets. Power BI recalculates it dynamically whenever either component measure changes due to filters.

Finally, create the Process Health Index. This is the most complex measure because it counts all flagged invoices and expresses the healthy percentage.

  • Create one final measure:
Process Health Index = (1 - DIVIDE(CALCULATE(COUNTROWS(InvoiceLogs), OR(InvoiceLogs[Flag_DuplicateInvoice] = TRUE(), OR(InvoiceLogs[Flag_NoPO] = TRUE(), InvoiceLogs[Flag_LatePayment] = TRUE()))), COUNTROWS(InvoiceLogs))) * 100
  • Press Enter to confirm. You should see all six measures listed under the InvoiceLogs table in the Fields pane.
  • Save your file with Ctrl+S.

How does Process Health Index work?

The formula counts every invoice that has at least one anomaly flag (duplicate, no PO, or late payment). It divides that count by total invoices to get the "problem ratio." Subtracting from 1 flips it into a health score. Multiplying by 100 expresses it as a percentage.

A score of 100 means zero flagged invoices. A score of 75 means 25% of invoices have at least one anomaly. Your dataset should produce a score around 75-80, matching the output from your Python script.

Seeing a formula error?

DAX is strict about parentheses. If you see a red underline or error message, count your opening and closing parentheses. The Process Health Index formula has 5 opening and 5 closing parentheses.

Also make sure your table is named exactly InvoiceLogs (not the original invoice_logs_flagged). Column references like InvoiceLogs[Flag_DuplicateInvoice] must match the table name you set earlier.

Help me fix my DAX formula error

Your data model is now fully wired. You have three connected tables and six measures ready to power your dashboard visuals. Next up, you will design the Home dashboard page with a gauge, KPI cards, and trend charts that bring these numbers to life.

Design the Home Dashboard Page

Your data model is solid. You have tables connected by relationships and DAX measures that calculate every KPI a finance stakeholder cares about.

Now it is time to surface those numbers in a visual format that a CFO can scan in five seconds. In this step, you will build the Home page of your dashboard with a Process Health Index gauge, four leakage KPI cards, a monthly trend line, a breakdown donut chart, and slicers for interactive filtering.

In this step, get ready to:
  • Create a Gauge visual showing Process Health Index with color-coded thresholds.
  • Build four KPI Card visuals highlighting capital leakage metrics.
  • Add trend, breakdown, and slicer visuals for interactive exploration.
Create the Process Health Index gauge

The Process Health Index is a single number that communicates overall P2P health at a glance. A gauge visual is the perfect fit because it shows progress toward a benchmark target.

  • Switch back to Power BI Desktop with your P2PAudit.pbix file open.
  • Make sure you are on the **Report** view (the bar chart icon on the left sidebar).
  • Click on an empty area of Page 1 to deselect any visual.
  • In the Visualizations pane on the right, click the Gauge icon (it looks like a speedometer).
  • A blank gauge visual appears on the canvas.
  • In the Visualizations > Build visual pane, drag your Process Health Index measure into the Value field well.

The gauge now shows a value, but the scale defaults are wrong. You need to set the min, max, and target manually.

  • With the gauge selected, click the Format visual icon (the paint brush) in the Visualizations pane.
  • Select the Visual tab.
  • Expand the Gauge axis section.
  • Set Min to 0.
  • Set Max to 100.
  • Set Target to 95. This represents the industry benchmark for healthy P2P processes.

Why set the target to 95?

A Process Health Index of 95% means that only 5% of invoices carry anomalies. This is a common internal audit benchmark. The needle on the gauge shows at a glance whether the organization is meeting or falling short of that standard.

Next, add conditional formatting so the gauge fill color changes with the score.

  • Still in the Format visual > Visual tab, expand the Colors section (under the Gauge axis section).
  • Next to the fill color option, click the fx button to open the conditional formatting dialog.
  • Set Format style to Rules.
  • Set What field should we base this on? to your Process Health Index measure.
  • Add three rules:
  • Click OK to apply the conditional formatting.
  • Position the gauge in the upper-left area of the canvas by dragging it.
  • Resize it to approximately one-quarter of the page width.

Your gauge should now display the Process Health Index value with a target needle at 95, and the fill color reflects the health zone (green, amber, or red).

Gauge showing a blank or wrong value?

  • Make sure you dragged the Process Health Index measure (not a column) into the Value well. Measures appear with a calculator icon.
  • If the gauge reads 0, check that invoice_logs_flagged.csv loaded correctly in the Data view. The flag columns need TRUE or FALSE values (not blank).
  • If conditional formatting rules don't appear, confirm you clicked the fx button next to the color option (not just the color picker swatch).

Help me troubleshoot my gauge visual.

Build the Leakage Summary KPI cards

The KPI cards sit across the top of the page as headline metrics. Finance stakeholders look here first to see how much money is leaking.

Before adding the cards, you need one more measure for the "Invoices Without PO" count. This was not created in the previous step.

  • In the Data pane on the right, right-click the InvoiceLogs table.
  • Select New measure.
  • Enter the following DAX formula in the formula bar:
Invoices Without PO = CALCULATE(COUNTROWS(InvoiceLogs), InvoiceLogs[Flag_NoPO] = TRUE())

What does this measure do?

This counts only the rows where Flag_NoPO is TRUE. These represent maverick spend invoices that were processed without a purchase order. Finance teams track this number because it indicates procurement bypass.

Now create the four Card visuals.

  • Click on an empty area of the canvas.
  • In the Visualizations pane, click the Card visual icon.
  • Drag your Total Leakage measure into the card's Fields well.
  • Position this card at the top of the page, to the right of the gauge.
  • Repeat to create three more Card visuals for:

Format the currency cards so they display in dollar format.

  • Select the Total Leakage card.
  • In the Format visual pane (paint brush icon), go to the General tab.
  • Expand Data format.
  • Set the format to Currency.
  • Repeat for the Duplicate Payment Amount and Late Payment Penalties cards.

Now apply red font color conditional formatting to the three leakage cards.

  • Select the Total Leakage card.
  • In the Format visual pane, switch to the Visual tab.
  • Expand the Callout value section.
  • Change the font Color to red.
  • Repeat for the Duplicate Payment Amount and Late Payment Penalties cards.
  • Leave the Invoices Without PO card in its default color (it shows a count, not a dollar leakage amount).

You should now see four cards at the top of your page. The three leakage cards display red values, drawing immediate attention to financial risk.

Cards showing a blank value?

  • Ensure you dragged a measure (calculator icon) and not a column into the card. Columns without aggregation display blank in cards.
  • If your Invoices Without PO card shows an error, double-check the DAX formula references InvoiceLogs[Flag_NoPO] exactly. Column names are case-sensitive.

Help me fix my KPI card visuals.

Add trend and breakdown visuals

The gauge and cards give the headline. Now you need visuals that let stakeholders explore the data across time and categories.

  • Click an empty area of the canvas below the KPI cards.
  • In the Visualizations pane, click the Line chart icon.
  • Drag DateTable[Month] into the X-axis well.
  • Drag your Total Leakage measure into the Y-axis well.
  • The line chart shows how leakage fluctuates month over month. Position it on the lower-left of the canvas.

Why a line chart for leakage trends?

Line charts excel at showing change over time. Finance teams use monthly trend lines to spot seasonal spikes (like quarter-end rushes that cause more duplicate invoices) and to measure whether controls are improving over time.

Next, add a donut chart to show the breakdown of leakage by anomaly type.

  • Click an empty area of the canvas to the right of the line chart.
  • In the Visualizations pane, click the Donut chart icon.
  • For the donut chart, you need to show three categories: Duplicate, Late Payment, and No PO. Create a simple visual by dragging the three flag measures into the Values well:
  • Position the donut chart to the right of the line chart.

Finally, add slicers so users can filter the entire page interactively.

  • Click an empty area at the top of the canvas (above or beside the gauge).
  • In the Visualizations pane, click the Slicer icon.
  • Drag DateTable[Year] into the slicer's Field well.
  • Create a second slicer and drag DateTable[Quarter] into its Field well.
  • Create a third slicer and drag Vendors[VendorCategory] into its Field well.
  • Arrange all three slicers neatly at the top or side of the page.

What do slicers do?

Slicers are interactive filter controls. When a user clicks a year, quarter, or vendor category in a slicer, every other visual on the page updates instantly to show only the filtered data. This is what makes a Power BI dashboard interactive rather than static.

  • Test your slicers by clicking a specific year (e.g. 2024) in the Year slicer.
  • Confirm that the gauge, cards, line chart, and donut chart all update to reflect the filtered data.
  • Click the year value again to deselect it and return to the full view.
  • Save your file by pressing Ctrl+S (Windows) or Cmd+S (macOS).

Your Home dashboard page is now complete. You should see the gauge on the left, four KPI cards across the top, a leakage trend line chart, a donut breakdown chart, and three slicers for interactive filtering.

Slicers not filtering other visuals?

  • This usually means the relationships between tables are not configured correctly. Go to the Model view and confirm that DateTable is related to InvoiceLogs[InvoiceDate] and that Vendors[VendorID] is related to InvoiceLogs[VendorID].
  • Make sure the filter direction goes from the dimension table (DateTable, Vendors) to the fact table (InvoiceLogs). The arrow on the relationship line should point toward InvoiceLogs.

Help me fix my slicer filtering issue.

Your Home page now tells a complete story at a glance. A finance stakeholder can see overall process health, identify exactly where money is leaking, track trends over time, and drill into specific categories. Next up, you will build a drill-through page that lets users right-click any vendor to see detailed risk information.

Build the Vendor Risk Drill-Through Page

Your Home page gives finance stakeholders an at-a-glance view of process health and total leakage. That answers the question "how bad is it?" but not "who is causing it?"

In this step, you will build a drill-through page that lets stakeholders right-click any vendor on the Home page and jump directly to a detailed risk profile. This page surfaces the specific invoices, dollar amounts, and flag counts that make a vendor high-risk.

In this step, get ready to:
  • Create a drill-through page with a Back button for navigation.
  • Build a vendor risk matrix with conditional formatting.
  • Add detail visuals for individual vendor investigation.
Create the drill-through page

A drill-through page in Power BI is a hidden detail page that users reach by right-clicking a data point on another page. It automatically receives the filter context of the item the user clicked.

  • In Power BI Desktop, click the + icon next to your existing page tab at the bottom of the canvas to add a new page.
  • Double-click the new page tab at the bottom of the window.
  • Type Vendor Risk Detail and press Enter to rename it.
  • In the Visualizations pane on the right, scroll down to find the Drill through section.
  • Drag VendorName from the InvoiceLogs table in the Data pane into the Add drill-through fields area.

What does this do?

When you add a field to the drill-through area, Power BI automatically creates a Back button on the page. Users on the Home page can now right-click any vendor name in a visual, select Drill through from the context menu, and choose Vendor Risk Detail to navigate here with that vendor pre-filtered.

  • Confirm a Back button appeared automatically in the top-left corner of the page.
  • If you do not see it, add one manually by clicking Insert in the ribbon, then Buttons, then Back.

Don't see the Drill through section?

Make sure nothing is selected on the canvas. Click an empty area of the page first, then check the Visualizations pane. The Drill through section appears below the Filters section when no visual is selected.

I can't find the Drill through section in the Visualizations pane.

Build the high-risk vendor matrix

The matrix visual lets you display vendors as rows with their risk metrics as columns. Conditional formatting turns it into a heat map so the highest-risk vendors jump out immediately.

  • Click an empty area of the Vendor Risk Detail page.
  • In the Visualizations pane, click the Matrix icon (a grid of rows and columns).
  • Drag the visual to fill the left half of the canvas.
  • From the Vendors table, drag VendorCategory into the Rows well first.
  • Then drag VendorName into Rows below it to create a hierarchy.
  • From the measures you created, drag Total Invoice Amount into the Values well.
  • Drag Duplicate Payment Amount into Values.
  • Drag Late Payment Penalties into Values.

You also need a measure to count the number of flagged invoices per vendor. This does not exist yet, so you will create it now.

  • Create a new measure for the flagged invoice count by clicking New measure in the Home ribbon tab and entering this DAX formula:
Flagged Invoice Count = 
CALCULATE(
    COUNTROWS(InvoiceLogs),
    InvoiceLogs[Flag_DuplicateInvoice] = TRUE()
        || InvoiceLogs[Flag_NoPO] = TRUE()
        || InvoiceLogs[Flag_LatePayment] = TRUE()
)

What does this measure do?

This measure counts all rows in the InvoiceLogs table where at least one anomaly flag is TRUE. The CALCULATE function modifies the filter context to only include flagged rows before counting.

When placed inside a matrix with VendorName on the rows, it automatically evaluates per vendor thanks to Power BI's row-level filter context.

  • Drag the new Flagged Invoice Count measure into the Values well of the matrix.

Now apply conditional formatting to make high-risk vendors visually obvious.

  • In the matrix visual, right-click the Flagged Invoice Count column header.
  • Select Conditional formatting, then Background color.
  • In the dialog, set Format style to Rules.
  • Add a rule: If value is greater than 3, set the background to red.
  • Add a rule: If value is between 1 and 3, set the background to amber/yellow.
  • Add a rule: If value equals 0, set the background to green.
  • Click OK to apply.

Why conditional formatting on the flag count?

Finance stakeholders scan large tables quickly. Color-coded cells let them instantly identify which vendors need attention without reading every number. Red means immediate investigation, amber means monitor, green means compliant.

  • Repeat the same conditional formatting process for the Duplicate Payment Amount and Late Payment Penalties columns.
  • For these dollar-amount columns, use a threshold of $10,000 for the red rule.
  • Click the drill-down arrows in the matrix header to confirm you can expand from VendorCategory into individual VendorName rows.

Matrix showing blank or no data?

Make sure you dragged VendorCategory from the Vendors table (not from InvoiceLogs). Also confirm your relationship between InvoiceLogs and Vendors on VendorID is active in the Model view.

Help me troubleshoot my blank matrix visual.

Add vendor detail visuals

When a stakeholder drills through to a specific vendor, they need context beyond the matrix. You will add a card showing the vendor name, a table of individual flagged invoices, and a trend chart.

  • Create a new measure for the selected vendor's name by clicking New measure in the Home ribbon and entering this DAX formula:
Selected Vendor = SELECTEDVALUE(InvoiceLogs[VendorName], "All Vendors")

What is SELECTEDVALUE?

The SELECTEDVALUE function returns the single value of a column when the current filter context has exactly one distinct value. If multiple or zero values are selected, it returns the alternate result you provide (in this case, "All Vendors").

When a user drills through to this page for a specific vendor, the filter context contains only that vendor's name. The card displays it as a clear header.

  • Add a Card visual to the top-right of the page.
  • Drag the Selected Vendor measure into the card's Fields well.
  • Increase the font size of the card's callout value to 28pt in the Format visual pane so the vendor name is prominent.

Next, add a table showing the individual flagged invoices for the selected vendor.

  • Add a Table visual below the Card.
  • Drag these columns from InvoiceLogs into the table's Columns well: InvoiceNumber, InvoiceDate, Amount, Flag_DuplicateInvoice, Flag_NoPO, Flag_LatePayment.
  • In the Filters on this visual section of the Filters pane, add a filter on any of the flag columns set to TRUE so only flagged invoices appear.

Finally, add a bar chart showing the vendor's invoice volume over time.

  • Add a Clustered bar chart visual to the bottom-right of the page.
  • Drag InvoiceDate into the X-axis well.
  • Drag Total Invoice Amount into the Y-axis well.
  • Set the date hierarchy level to Month by clicking the hierarchy drill-down arrow in the visual header until you reach the monthly level.
  • Save your file by pressing Ctrl+S.

To test the drill-through, switch to the Home page.

  • Right-click any vendor name in a visual on the Home page.
  • Select Drill through, then choose Vendor Risk Detail.
  • The detail page should filter to show only that vendor's data across all visuals.

Drill through option not appearing on right-click?

The right-click context menu only shows Drill through when you click a data point that matches a field in the drill-through filter area. Make sure you are right-clicking a vendor name value in a visual (not an axis label or blank area).

Also confirm that VendorName is in the drill-through fields on the Vendor Risk Detail page.

Help me fix my drill-through configuration.

Your dashboard now has two layers of analysis. The Home page gives the high-level picture, and the Vendor Risk Detail page lets stakeholders dig into specific problem vendors. Next up, you will translate all of these findings into an executive summary presentation.

Create the Executive Summary Presentation

Your dashboard is complete. You have an interactive Home page with a Process Health Index gauge, Leakage Summary KPI cards, and a Vendor Risk Detail drill-through page.

Now it is time to translate those insights into a story that finance stakeholders can act on. In this step, you will create a 5-slide executive summary presentation that follows the classic audit narrative: what is broken, what it costs, how to fix it, and what you gain.

In this step, get ready to:
  • Structure a 5-slide executive deck with a clear narrative arc.
  • Draft talking points for each slide using your audit data.
  • Export visuals from Power BI and embed them in your presentation.
Structure the 5-slide deck

A strong executive presentation follows a predictable story arc. Each slide advances the narrative from problem to solution so that stakeholders stay engaged and arrive at a clear recommendation.

  • Open Microsoft PowerPoint (or Google Slides if you prefer).
  • Create a new blank presentation.
  • Save it to p2p-audit/presentation/ with the filename P2P_Audit_Executive_Summary.pptx.

Set up the following 5 slides with these titles:

  • Slide 1 (Title slide): P2P Process Audit: Q1-Q4 2025 Findings.
  • Slide 2: Current State Challenge.
  • Slide 3: Root Cause Analysis.
  • Slide 4: Proposed Solution: Automated PO-Matching Rules.
  • Slide 5: Implementation Roadmap + Expected ROI.

Why only 5 slides?

Finance executives have limited attention spans for operational reviews. A 5-slide arc forces you to prioritize the most impactful data points. Each slide answers one question: What is happening? What does it cost? Why? How do we fix it? What do we gain?

Draft the narrative for each slide

Each slide needs talking points that connect your data to a business decision. Use the metrics from your Power BI dashboard to fill in the specific numbers. Frame the story as: broken, costs, fix, gain.

  • On Slide 2 (Current State Challenge), add these data points as bullet text:
  • On Slide 3 (Root Cause Analysis), plan space for:
  • On Slide 4 (Proposed Solution), write these talking points:
  • On Slide 5 (Implementation Roadmap + Expected ROI), add:

What is the narrative arc?

The story flows in four beats. Slide 2 shows what is broken. Slide 3 shows what it costs and why. Slide 4 proposes the fix. Slide 5 shows what you gain.

This structure mirrors how auditors present findings to CFOs. Leading with the problem creates urgency. Ending with ROI makes the recommendation easy to approve.

  • Save your presentation after adding all the text content.
Export visuals from Power BI and embed them

Your presentation needs the actual charts and tables from your dashboard, not recreated versions. Power BI Desktop lets you copy any visual as an image and paste it directly into PowerPoint.

  • Switch back to Power BI Desktop with your P2PAudit.pbix file open.
  • On the Home page, click on your Donut chart (anomaly type breakdown) to select it.
  • Click the ... (More options) icon in the upper-right corner of the visual.
  • Select Copy visual as image.
  • Switch to your PowerPoint presentation, navigate to Slide 3, and press Ctrl+V to paste the chart.
  • Repeat this process for additional visuals:

What does Copy visual as image do?

This copies a high-resolution static snapshot of the visual to your clipboard. The pasted image in PowerPoint will reflect whatever filters and slicer values were active at the time you copied. It preserves conditional formatting colors and layout.

  • Resize and position each pasted visual on its respective slide.
  • Add text annotations or callout arrows pointing to key data points in the visuals.
  • Save the final presentation to p2p-audit/presentation/P2P_Audit_Executive_Summary.pptx.

Use Current values before copying

If your dashboard has Year or Quarter slicers, set them to the values you want to present (e.g. 2025, all quarters) before copying visuals. The copied image captures whatever state the visual is in at that moment.

Open your final saved file and flip through all 5 slides. Each slide should have both visual evidence from Power BI and written talking points that explain what the audience is looking at.

Pasted visuals look blurry?

Make sure you are using Copy visual as image from the ellipsis menu, not taking a manual screenshot. The built-in copy produces a higher resolution image. If the visual still looks small, resize it in PowerPoint by dragging a corner handle while holding Shift to maintain the aspect ratio.

Help me get clearer visuals from Power BI into my presentation.

Secret mission

Add a Vendor Similarity Score Using Fuzzy Matching

Your duplicate invoice flags catch exact matches, but what about vendors hiding under slightly different names? In this bonus challenge, you'll build a Python script that uses fuzzy string matching to surface near-duplicate vendor names and visualize them in a dedicated Power BI page.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The p2p-audit/ project folder (Python scripts, CSV data files, Power BI report, executive presentation).
  • Power BI Desktop (installed application).
  • Python packages (pandas and faker).

Keep everything running

No action needed. Choose this if you plan to continue refining your dashboard, adding more anomaly detection logic, or extending the presentation.

  • Your p2p-audit/ folder stays on your Desktop with all scripts, data, and reports intact.
  • Power BI Desktop remains installed and ready for use.
  • Python packages remain available for future projects.

Pause - I'll come back to this later

Close running applications to free up memory. Your files and installations stay in place for when you return.

  • Close Power BI Desktop if it is still open.
  • Close any terminal windows you used during the project.
  • Your p2p-audit/ folder and all installed packages remain untouched.

Delete - I don't want to use this again

Remove all project files, uninstall packages, and optionally remove Power BI Desktop.

  • Delete the project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf p2p-audit

On Windows, run:

cd ~\Desktop
rmdir /s /q p2p-audit
  • Uninstall the Python packages by running:
pip uninstall pandas faker -y
  • Uninstall Power BI Desktop by opening Windows Settings, selecting Apps, then Installed apps. Search for Power BI Desktop and click Uninstall.
  • Confirm the project folder is gone by running:
ls ~/Desktop

You should no longer see p2p-audit in the output.

Nice Work!

Nice Work!

You just built a complete Procure-to-Pay financial audit from scratch. This is the type of deliverable that analytics engineers present to finance operations teams at mid-to-large enterprises.

You've learned how to:

  • Generate a realistic 10,000+ row mock ERP dataset with intentionally injected P2P anomalies using Python and Faker.
  • Build a multi-page Power BI dashboard with a Process Health Index gauge, Leakage Summary KPI cards, and a vendor risk drill-through matrix with conditional formatting.
  • Create an executive summary presentation that frames data findings as a business case for automated PO-matching rules.
  • Built a vendor similarity scoring system using fuzzy matching to surface hidden duplicate vendors in the vendor master.

Ready to quiz yourself?