Mini Spreadsheet — A Free Lightweight Desktop Spreadsheet for Windows

Sometimes Excel is overkill. You just need a grid, a few numbers, and a sum. Opening a full spreadsheet application, creating a file, saving it somewhere — it’s too much friction for a quick calculation.

I built Mini Spreadsheet for exactly that. It sits in your system tray, out of the way, until you need it. One click and you have a clean 25-row grid ready to go. No file to save, no ribbon to navigate.

Anyone who has worked with Excel knows the tax it charges just to open it. The OLE initialization, the random pauses, the ticks and stutters before you can type a single number. Half the time you just need a column of numbers added up — you don’t need all of that. Mini Spreadsheet opens instantly, no loading screens, no file dialogs, no surprises. And when you do need to take it further — share it, format it, or run a pivot — one click sends everything straight to Excel, formulas and all.

What It Does

Mini Spreadsheet is a lightweight Windows desktop app with a 5-column, 25-row grid and real formula support. It is not trying to replace Excel — it is trying to replace the calculator.

  • Formulas: =A1+B2, =A1-B2, =SUM(A1:A25)
  • Click a cell while typing a formula to insert its reference
  • Drag across cells to select a range (e.g. A1:C10)
  • Shift+Arrow to extend a range from the keyboard
  • Live column totals in the SUM footer — always visible
  • Numbers formatted with commas automatically
  • One-click Export to Excel if you need to take it further
  • Minimises to the system tray — always one click away
  • Optional: launch automatically at Windows startup

Who It’s For

Anyone who finds themselves opening Excel just to add up a column of numbers. Accountants doing a quick sanity check. Project managers rough-costing something on the fly. Anyone who wants a persistent scratchpad that is always one click away and never asks you to save a file.

Free for personal use. Copyright © 2024–2026 diyaccountant.ca. Redistribution or modification is not permitted.

How to Get It

Download the zip file below. It includes a standalone Windows executable (no Python required), a Python script version for technical users, and a full README.

Download Mini Spreadsheet v1.1

Windows 10/11 64-bit. Free for personal use.

Legal Email Discovery Tool — Search Your Email Archive, Export to PDF

I built this tool to solve a specific problem: searching through tens of thousands of emails to find the ones that matter — and exporting them cleanly as PDFs for legal or compliance use.

If you’ve ever had to respond to a litigation hold, conduct an internal investigation, or review a contract dispute, you know the pain. Gmail’s search is fine for day-to-day use. It is not designed for discovery.

There are other MBOX viewer tools out there. I tried them. The problem is that real-world MBOX files — especially Gmail Takeout exports from active accounts — can be multi-gigabyte files with tens of thousands of emails. Most tools choke on them. They freeze, crash, or simply never finish loading. I needed something that could handle the size without drama. So I built it myself.

This tool is lightweight, fast, and built for exactly this one job. It indexes your MBOX file once, then searches in seconds — no matter how large the archive. No bloat, no subscription, no cloud upload. Everything stays on your machine.

What Is an MBOX File?

An MBOX file is the standard format used by Gmail Takeout, Thunderbird, Apple Mail, and most other email clients to export or archive email. If you have ever downloaded your Gmail data through Google Takeout, you have an MBOX file. The problem is that these files can contain tens of thousands of emails, and there is no easy built-in way to search through them on Windows. You cannot just open them and Ctrl+F your way to the email you need.

This tool solves exactly that. Point it at your MBOX file, type a keyword, a name, a date range — and it finds the emails instantly.

What It Does

The Legal Email Discovery Tool searches one or more MBOX files — Gmail Takeout exports, Thunderbird archives, or any standard .mbox file — for emails matching your criteria, then exports each matching email as a properly formatted PDF.

  • Filter by sender, recipient, subject, body text, or date range
  • Combine multiple conditions: AND within a group, OR between groups
  • Handles archives of any size — tested on multi-GB files
  • Builds a persistent SQLite index so repeat searches take seconds
  • Crash-safe: saves progress every 50 messages and resumes where it left off
  • Outputs one PDF per matching email, plus a CSV log with full metadata
  • Works out of the box with Gmail Takeout exports

Who It’s For

Free for personal use. If you’re a law firm, litigation support provider, or using it professionally, a commercial license is available for $20/year — which also gets you a stamped, verifiable license on every PDF you produce.

How to Get It

Download the zip file below. It includes a standalone Windows executable (no Python required), a Python script version for technical users, and a full README with step-by-step instructions.

⬇ Download Legal Email Discovery Tool v1.6

Windows 10/11 64-bit. Free for personal use.

Automate or Fix? Here’s How I Actually Decide

A lot of people ask me about automation. Specifically, when do you automate something and when do you just fix it?

Honestly, I wasn’t sure I had a clean answer. But then I thought about how we actually make these decisions and I think there’s a framework buried in there. Let me walk through it using a real example.

The Invoice Problem

We have a vendor that sends us hundreds of invoices. For a long time, the AP team was entering them manually. Tedious, error-prone, inconsistent descriptions in the GL (i.e. the same product can be described as “muffler” or “chrome muffler” or “Muffler” or “MUFFLER” which made searching a minefield) and a waste of a good person’s time.

Most companies in this situation would do one of two things. They either keep entering them manually, or they buy some kind of OCR or AI system to read the PDF invoices and extract the data automatically. That second option sounds smart. It isn’t.

Here’s why. PDF extraction is not accurate. You have to train the system. It still makes mistakes. And it costs money and sometimes a lot of it. At the end of the day, no one is 100% certain it is right and the person reviewing it has to be extra careful.

We did something different. We went to the vendor directly and asked them to export a flat Excel file of our invoices instead of just emailing us PDFs. That’s it. That was the fix.

They have sophisticated systems just like we do. The data already exists in a clean, structured format on their end. We just asked them to share it. It took one conversation. It costs nothing. And it works perfectly every time.

From there, we built a simple Power Automate flow that takes that file and creates the purchase invoices directly in Business Central — matched against the PO automatically. The AP person still gets the PDF in their email, but their job is now to verify what’s already in the system, not to enter it. That’s a fundamentally different job.

Result: about 80% reduction in data entry time. Zero typos. Zero cost. A few hours of setup.

That’s low hanging fruit.

So How Do You Decide?

Working through that example, here’s what I think matters:

Go upstream, not around the problem.

The obvious solution was to build technology around the PDF. The better solution was to eliminate the PDF problem entirely by going to the source. Before you automate anything, ask yourself: where did this mess come from? Can you fix it there instead of building around it?

Most of the time, you can. And it’s almost always simpler and cheaper.

Only automate what is stable, repetitive, and rule-based.

Our invoice flow works because the input is consistent. Same vendor, same format, same fields every time. The rules don’t change.

Things that aren’t worth automating: one-off invoices; processes that change constantly; anything that requires judgment. If a human needs to think about it every time, a computer probably should not be doing it.

Fix the process before you automate it.

This one is important. You cannot automate a broken process. If you try, you just get a faster broken process.

If something isn’t working, put humans back in the loop. Figure out what’s wrong. Clean it up. Run it for a while. Then automate. In that order.

Keep humans in the loop deliberately.

We don’t auto-post the invoices. A person reviews them first. That’s intentional.

The goal isn’t to remove humans from the process, it’s to change what they’re doing. Instead of entering data, our AP person is now catching operational issues, flagging discrepancies, asking questions like “why did we buy so much of this?” That’s an AP Financial Analyst, not an AP clerk. Same person, better job.

Automation should elevate the role, not just reduce the hours.

You need to own it yourself.

Here’s the part most people don’t talk about. When the automation breaks, and at some point it will break, you need someone on the team who can fix it. Not a consultant. Not a ticket to IT. Someone who actually understands what the flow does and can get it running again the same day. Some of the people on our team join as accountants first, but end up being accountants with a tech side job. They become real wizards with the tools that are already accessible but never used. Just think how many people have access to Power Automate already, but never even try to open it.

I happen to build some of these automations myself, so that’s not an issue for me. But not every team has that. I think it should be a core skill for any CFO. If you can’t fix your own automation, you don’t really own it. Once your team gets a whiff of the automations, a few of the team members will really latch on to it. They will come up with ideas. They will bring forward new automations and some might even try it on their own and become champions of their own. Your job is to show them a path, and give them room to breathe. The one thing you need to do is to know what the processes and automations are, just in case something breaks; you will know where to look and how to fix it.

Not All Automation Has to Be Smart

At home, I run a mix of motion sensor switches and Z-wave switches controlled through Home Assistant. Friends walk in and inevitably ask: why not put Z-wave switches everywhere?

The answer is simple. I put motion sensor switches in the places where I want the light to come on when I walk in. That’s all I need them to do. Why add a Z-wave switch, set up automations in Home Assistant, configure scenes and schedules — when a $20 motion sensor switch does exactly the same job without any of that?

From the outside, the house seems equally automated either way. But some of that automation is just a dumb motion sensor doing one thing reliably. And there’s nothing wrong with that. In fact, that’s the whole point.

I bring the same logic to work. The invoice automation we built isn’t sophisticated. It’s a flat Excel file fed into Power Automate. No AI, no machine learning, no expensive software. Just a file that gets picked up and turned into purchase invoices. It looks like automation. It is automation. But it’s not complicated.

Don’t reach for the fancy switch when the motion sensor will do.

The Simple Test

Before I automate anything, I ask three questions:

  1. Is this high volume, consistent, and rule-based?
  2. Have I fixed the process first, or am I automating around a problem?
  3. Can someone on my team maintain this without me?

If the answer to all three is yes, it’s worth automating. If not, fix it first, or don’t automate it at all.

The best automations are the ones that are almost boring in how simple they are. Elegant, really. If it feels complicated to explain, it’s probably not ready.

This is part of an ongoing series on how I think about finance systems, automation, and building companies that run well. I’m a CFO and CA CPA who likes to tinker — and occasionally write about it.

How to Connect Power Automate to Business Central Web Services Using oAuth2.0

Power Automate has Business Central connectors, however, they are very limited. What Power Automate does not have are connectors to Business Central for the Web Services. You can locate these web services in Business Central under Web Services.

Services listed under Web Services in Business Central

So how can you get access to the Web Services in Power Automate? Not that easy. But I figured it out with some sleuthing and piecing things together. This is a multi-step process and if you do not have admin rights to Azure/Business Central, you may need to contact your IT department to get them.

So the steps (read difficulties) I ran into getting this to work are the following:

  1. You need to authenticate against Business Central (Microsoft) using oAuth2.0 and get a bearer token
  2. You use the bearer token to call the web service that you need
  3. You get a JSON response
  4. You need to fiddle with the response and the Parse JSON did not work properly for me so I did need to MacGyver some of it
  5. You get variables and you can use them anywhere, in my case, I just wanted to populate an Excel table

Here is what I did:

  1. oAuth2.0 and Power Automate
  • Now click on API permissions and make sure these permissions are added. So click on Add a permission and on the next screen select Microsoft APIs and look for Business Central. Select Business Central and then select DELEGATED PERMISSIONS. I selected app_access/Automation.ReadWrite.All/user_impersonation. I do not believe that you need to Financials.ReadWrite.All.
  • After you are done that, you need to add another permission but this time, you need to choose Microsoft Graph. The select Delegated Permission and then add USER.READ.
  • After you have done all that Don’t forget to GRANT ADMIN CONSENT after you add it!
  • Click on the Overview tab, and make a note of the Client ID and Tenant ID. We will need those in a bit.

Now, go to Power Automate where we will create the create the steps required to access Business Central

  • This is a fairly simple process and here is the Power Automate steps that we will require:
  • Initialize a variable and assign it a name Username or UN. It is a STRING variable and paste the CLIENT ID that you got from Azure into the value field.
  • Initialize another variable and assign it a name Password or PW. It is also a STRING variable and paste the SECRET (password) that you got from Azure into the value field
  • Add a new step using COMPOSE. Add this line into the Expression (with all the quotes, but replace XXXX with the Client ID from above and YYYY with the Secret (password) from above:
  • Now add an HTTP step
    • Under HEADERS add Content-Type as a key and assign this to the value: application/x-www-form-urlencoded
    • Under QUERIES add client_id and client_secret as keys, and assign the values we got from above into them
    • Under BODY put in the output from the COMPOSE step
    • Now click on ADVANCED OPTIONS and use Authentication: BASIC and put in the username and password variables into username and password
  • Add a new step and choose PARSE JSON. Use the Body from the previous step and use this as the schema:
{
    "type": "object",
    "properties": {
        "token_type": {
            "type": "string"
        },
        "expires_in": {
            "type": "integer"
        },
        "ext_expires_in": {
            "type": "integer"
        },
        "access_token": {
            "type": "string"
        }
    }
}
  • Add a new step HTTP again
    • This time use GET as the Method
    • the URI comes from the Business Central Web Services APIv2.0. Generally, the calls look like this (where xxxx is your tenant ID / cccc is your company name from business central / dddd is the webservice name that you gave: https://api.businesscentral.dynamics.com/v2.0/xxxx/Production/ODataV4/Company(‘cccc’)/dddd
    • Under HEADERS add 3 keys and assign these values:
      • Content-Type value to assign application/json
      • Authorization value to assign Bearer and choose the variable “access token” from our previous step
      • Accept: value to assign odata=nometadata
    • Under ADVANCED OPTIONS, choose None for authentication
  • The step above will return the information to you in a JSON format. In my case, I got the CUSTOMER CARD information from Business Central (this is Page 21)
  • Add another step and use the Parse JSON function. Use the Body from the HTTP request above under CONTENT. Use this as the schema (note you may have additional fields for your customization):
{
    "body": {
        "type": "object",
        "properties": {
            "@@odata.context": {
                "type": "string"
            },
            "value": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "@@odata.etag": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Name": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Name_2": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Search_Name": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "IC_Partner_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Balance_LCY": {
                            "type": "integer"
                        },
                        "Balance_Due_LCY": {
                            "type": "integer"
                        },
                        "Credit_Limit_LCY": {
                            "type": "integer"
                        },
                        "Blocked": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Privacy_Blocked": {
                            "type": "boolean"
                        },
                        "Salesperson_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Responsibility_Center": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Service_Zone_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Document_Sending_Profile": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "TotalSales2": {
                            "type": "integer"
                        },
                        "CustSalesLCY_CustProfit_AdjmtCostLCY": {
                            "type": "integer"
                        },
                        "AdjCustProfit": {
                            "type": "integer"
                        },
                        "AdjProfitPct": {
                            "type": "integer"
                        },
                        "CFDI_Purpose": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "CFDI_Relation": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "CFDI_Export_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "SAT_Tax_Regime_Classification": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Last_Date_Modified": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Disable_Search_by_Name": {
                            "type": "boolean"
                        },
                        "Address": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Address_2": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Country_Region_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "City": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "County": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Post_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "ShowMap": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Phone_No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "MobilePhoneNo": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "E_Mail": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Fax_No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Home_Page": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Language_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Primary_Contact_No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "ContactName": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Bill_to_Customer_No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "VAT_Registration_No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "EORI_Number": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "GLN": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Use_GLN_in_Electronic_Document": {
                            "type": "boolean"
                        },
                        "Copy_Sell_to_Addr_to_Qte_From": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Tax_Liable": {
                            "type": "boolean"
                        },
                        "Tax_Area_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Tax_Identification_Type": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Tax_Exemption_No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "RFC_No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "CURP_No": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "State_Inscription": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Gen_Bus_Posting_Group": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "VAT_Bus_Posting_Group": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Customer_Posting_Group": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Currency_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Price_Calculation_Method": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Customer_Price_Group": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Customer_Disc_Group": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Allow_Line_Disc": {
                            "type": "boolean"
                        },
                        "Invoice_Disc_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Prices_Including_VAT": {
                            "type": "boolean"
                        },
                        "Prepayment_Percent": {
                            "type": "integer"
                        },
                        "Application_Method": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Partner_Type": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Payment_Terms_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Payment_Method_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Reminder_Terms_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Fin_Charge_Terms_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Cash_Flow_Payment_Terms_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Print_Statements": {
                            "type": "boolean"
                        },
                        "Last_Statement_No": {
                            "type": "integer"
                        },
                        "Block_Payment_Tolerance": {
                            "type": "boolean"
                        },
                        "Preferred_Bank_Account_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Bank_Communication": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Check_Date_Format": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Check_Date_Separator": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Ship_to_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Location_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Combine_Shipments": {
                            "type": "boolean"
                        },
                        "Reserve": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Shipping_Advice": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Shipment_Method_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Shipping_Agent_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Shipping_Agent_Service_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Shipping_Time": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Base_Calendar_Code": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Customized_Calendar": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "ExpectedCustMoneyOwed": {
                            "type": "integer"
                        },
                        "TotalMoneyOwed": {
                            "type": "integer"
                        },
                        "CalcCreditLimitLCYExpendedPct": {
                            "type": "integer"
                        },
                        "Balance_Due": {
                            "type": "integer"
                        },
                        "Payments_LCY": {
                            "type": "integer"
                        },
                        "CustomerMgt_AvgDaysToPay_No": {
                            "type": "integer"
                        },
                        "DaysPaidPastDueDate": {
                            "type": "integer"
                        },
                        "AmountOnPostedInvoices": {
                            "type": "integer"
                        },
                        "AmountOnCrMemo": {
                            "type": "integer"
                        },
                        "AmountOnOutstandingInvoices": {
                            "type": "integer"
                        },
                        "AmountOnOutstandingCrMemos": {
                            "type": "integer"
                        },
                        "CustInvDiscAmountLCY": {
                            "type": "integer"
                        },
                        "Global_Dimension_1_Filter": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Global_Dimension_2_Filter": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Currency_Filter": {
                            "type": [
                                "string",
                                "null"
                            ]
                        },
                        "Date_Filter": {
                            "type": [
                                "string",
                                "null"
                            ]
                        }
                    },
                    "required": []
                }
            }
        }
    }
}
  • Add a new step and initialize 1 variable for each field that you will require data for. In this case, we will just obtain the Name of the client. So initialize 1 variable and call it Name, type is STRING, and assign it any value for now
  • Add an Apply to Each step under SELECT AN OUTPUT FROM PREVIOUS STEPS I added this expression: outputs(‘Parse_JSON_DetailCustomer’)?[‘body’]?[‘value’]
  • Note that I called my Parse JSON step Parse JSON DetailCustomer so remember to add an underscore for all the spaces. You might have named that step something else, so replace the title of that step with yours. If you are at a loss, you can get the title just by looking at the Parse JSON step
  • Now assign the value to the variable by using the set variable step and using this expression: items(‘Apply_to_each’)?[‘Name’] Note that the Apply_to_each is the title for this step
  • Once you have assigned the variable, you can use it anywhere!

This entire process was frustrating on many levels. Microsoft documentation is about as good if it were written in an alien language (why is that?). There is great information about oAuth2.0 authentication (but only after spending hours looking) and not a great deal on Parse JSON. I ran into days of frustration when I kept on getting object not array errors, or null errors. In any event, this worked for me, and I hope it works for you.

How to Access Business Central Tables in Power Automate Using OData

If you are like me and do a lot of importing and sorting and just want data at your fingertips without having to go through Business Central, you like to have data either in SQL or in Excel. Why? Because you can do xlookups or other necessary evils when creating import tables with configuration packages or other automation.

So how do you go about getting the data into Excel? You can use PowerAutomate with Business Central.

I will show you the steps that I had to take to do this.

Step 1: Use the Business Central Connector in PowerAutomate and use the FIND RECORDS (V3). You will need to input the Environment Name (usually Production) and then the company name. Use the API v2.0, and for my case, I used the customers table.

Step 2: Add another step, APPLY TO EACH and use the output from the previous step. Choose the one that says VALUE (List of items). Then select EXCEL ONLINE BUSINESS > ADD A ROW INTO A TABLE and work your way through to the Excel file that you created for this. Remember, your data MUST sit in a table in Excel, so you have to formulate this first. Your flow should look like this, but your table may look different from mine.

Simply go through the different headers in your excel file and simply plop down the the data that you need for each one. You will have an excel file that looks like this in the end (I have obfuscated the confidential data)

PowerBI Master Calendar Script: A Complete Date Table for Power Query

The Master Calendar is super important in PowerBI, especially if you want to run reports using relative dates or a series in time. Essentially all tables, should be linked to your master calendar via the date field.

This is what PowerBI looks like when the dates are all joined. You should use the date field to connect to other date fields. It is usually a 1 to many relationship.

Master Calendar

The Master Calendar is important, especially for financial reporting because you can create columns that indicate fiscal year, fiscal period, week number, year, year to date flags, next month, last twelve months and so forth. Here is a Master Calendar that I created:

This Master Calendar includes the day relative to today, month relative to today, day of the week in words, fiscal year (which is different from the calendar year), fiscal quarter, accounting period (as a number), week number (relative to the calendar), weekday or weekend and so forth.

The Master Calendar allows you to use slicers to filter out data automatically. This is very important when creating financial reports that are a point in time, such as the balance sheet vs income statement which is a period of time. It is exceptionally useful when running trial balance data. If you are short on time, you can just use the steps below to create the Master Calendar in PowerBI:

  • Click on NEW TABLE (name the table Master_Calendar)
  • Make sure you mark the table as a date table by clicking on DATE TABLE
  • Then start by adding this column
    • Your first column is a column of dates automatically generated by PowerBI:
DATE=(CALENDAR(START DATE,TODAY())
  • Note, you can add to the number of days beyond today() by simply putting a + and the number of days you want to go beyond today. Like this:
    • DATE=(CALENDAR(START DATE,TODAY()+30)
  • Then add the following each as a new column:
	1. DATE=(CALENDAR(START DATE,TODAY())

	2. Day_of_week = Weekday('Date'[Date],2)

	3. Date relative to today = 1*([Date]-today())

	4. Day_of_week_name = IF('Date'[Day_of_week]=1,"Monday",IF('Date'[Day_of_week]=2,"Tuesday",IF('Date'[Day_of_week]=3,"Wednesday",IF('Date'[Day_of_week]=4,"Thursday",IF('Date'[Day_of_week]=5,"Friday",IF('Date'[Day_of_week]=6,"Saturday","Sunday"))))))

	5. Fiscal_Year = IF(MONTH([Date])>=8,YEAR([Date])+1,YEAR([Date]))

	6. Fiscal_Quarter = IF(AND(MONTH([Date])>=8,MONTH([Date])<=10),"Q1",IF(OR(MONTH([Date])>=11,MONTH([Date])<=1),"Q2",IF(AND(MONTH([Date])>=2,MONTH([Date])<=4),"Q3",IF(AND(MONTH([Date])>=5,MONTH([Date])<=7),"Q4"))))

	7. Accounting_Period = IF(MONTH([Date])=8,1,IF(MONTH([Date])=9,2,IF(MONTH([Date])=10,3,IF(MONTH([Date])=11,4,IF(MONTH([Date])=12,5,IF(MONTH([Date])=1,6,IF(MONTH([Date])=2,7,IF(MONTH([Date])=3,8,IF(MONTH([Date])=4,9,IF(MONTH([Date])=5,10,IF(MONTH([Date])=6,11,IF(MONTH([Date])=7,12))))))))))))

	8. Week_number = WEEKNUM([Date],2)

	9. Weekday_or_weekend = IF(AND([Day_of_week]>=1,'Date'[Day_of_week]<=5),"Weekday","Weekend")

	10. Month_relative = 12 * Year([Date])+Month([Date])-12*Year(today())-Month(today())

	11. Last_12_Months = IF(AND('Date'[Month_relative]>=-12,'Date'[Month_relative]<=-1),"LTM","")

	12. Fiscal_Year_Presentation = CONCATENATE("FY",[Fiscal_Year])

	13. Fiscal_Year_Quarter = CONCATENATE([Fiscal_Year_Presentation],[Fiscal_Quarter])

	14. Month = IF(MONTH([Date])=1,"January",IF(MONTH([Date])=2,"February",IF(MONTH([Date])=3,"March",IF(MONTH([Date])=4,"April",IF(MONTH([Date])=5,"May",IF(MONTH([Date])=6,"June",IF(MONTH([Date])=7,"July",IF(MONTH([Date])=8,"August",IF(MONTH([Date])=9,"September",IF(MONTH([Date])=10,"October",IF(MONTH([Date])=11,"November","December")))))))))))

	15. End_Of_Month = (EOMONTH([DATE],0))

	16. Start_Of_Month = STARTOFMONTH('Date'[Date])

	17. Month_Number = MONTH('Date'[Date])

	18. Fiscal_Year_Period = CONCATENATE("FY",CONCATENATE('Date'[Fiscal_Year],CONCATENATE(" - P ",'Date'[Accounting_Period])))

	19. Short_week_name = left('Date'[Day_of_week_name],3)

	20. Calendar_Year = year('Date'[Date])

You should have a Master_Calendar table similar to the one I have. This is a very important step to creating financial reporting (or other time based reporting in PowerBI.

Please note that the fiscal year in this examples starts on Aug 1. If you want to change the fiscal year start date simply change items 5, 6 and 7 to your corresponding fiscal year to the period.

Foreign Exchange Rates in Business Central: Using the Built-In Currency API

We wanted to see all of the foreign exchange rates in Power BI from Business Central for various reasons: 1) To make sure the exchange rates were being updated daily and, 2) to run reports on the exchange rates

Business Central stores the FX rates in table

To create the API, go to WEB SERVICES in Business Central and select NEW. The object type is page, and use Object ID 483 and publish. The URL will be produced and you use that as the oData source in PowerBI.

Woodworking: Coffee Table Built from WW2-Era Reclaimed Wood

This piece of wood was in pretty bad shape. I managed to stabilize it with PC Wood Hardener and epoxy. The rotting portion was very much a sponge. It sucked up about 3 bottles of wood hardener and lots of epoxy and varnish.

Here is an over exposed picture of the piece of wood.

I used a hand plane to plane the surface of the table flat. I then realized the right side of the wood was going to fall off due to the rot.

After planing over several days

I don’t typically like the epoxy effects that some people do, so I strictly wanted the epoxy to be structural and not a feature. I mixed in some deep red dye so that it would not be entirely noticeable.

I had to make legs for this thing. I noticed some cool joins that welders do, so I tried one myself.

Because the bottom of the wood was not planed to the same thickness, I had to improvise on the legs. I made one of the legs adjustable for level.

How to Query Business Central Tables Using PowerShell

I wanted a really simply way to access the tables in Business Central and to write the information of those tables into SQL database so that my team could access them directly from SQL.

Since I am just an accountant, I always find it very difficult when I find snippets of code but nothing is explained. So I will try my best to break everything down for other fellow accountants so that they can follow through.

Sure, there are many ways to do this, but I just wanted to try this. So here is my script. You can copy this script directly, and there are just a few things for you to change. Any thing that is underlined you will need to change.

#  References.
#  https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code
#  https://www.reddit.com/r/PowerShell/comments/9clts3/powershell_automation_with_oauth2/
#

#
# https://login.windows.net/2383c526-aef3-47e9-8487-37712e689168/oauth2/token?resource=https://api.businesscentral.dynamics.com

# Class to get an OAuth 2.0 authentication token from BC using a Password Grant.
#
class AADPasswordGrant {
    [string]$token                  #Token that we need to get to Authenticate with later
    [string]$tenantId               #BC Tenant ID
    [string]$clientId               #The ApplicationId that was registed for BC in AAD.   
    [string]$username               #BC username
    [string]$password               #BC Password 
    [System.Security.SecureString]$securePasswordStr #BC Password we will convert to a secure string later
    [string]$securePasswordBStr     #BSTR version of the secure password  
    [string]$clientSecret           #Key that was generated when registring BC in AAD
    [string]$grantType      = "password"  #This must be password so we are not challenged or have to use a form.  
    [string]$callbackUrl    = "https://127.0.0.1/auth-response"              #The same callback registered for BC in AAD
    [string]$accessTokenUrl = "https://login.windows.net/{tenantId}/oauth2/token"  #Url to request the token from
    [string]$resourceUrl    = "https://api.businesscentral.dynamics.com"           #The resource we want to talk to
    [string]$scopeUrl       = "https://api.businesscentral.dynamics.com"           #The resource we want to talk to
   
    AADPasswordGrant([string]$tenandId, [string]$clientId, [string]$clientSecret, [string]$userName, [string]$password) {
        $this.tenantId     = $tenandId
        $this.clientId     = $clientId
        $this.clientSecret = $clientSecret
        $this.userName     = $userName
        $this.securePasswordStr  = (ConvertTo-SecureString -String $password -AsPlainText -Force)
        $this.securePasswordBStr = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($this.securePasswordStr))
        $this.accessTokenUrl     = $this.accessTokenUrl.Replace('{tenantId}', $tenandId)
    }

    [void]TryGetAuthorisationToken () {
        $body = @{
            grant_type    = $this.grantType
            username      = $this.userName
            password      = $this.securePasswordBStr
            client_id     = $this.clientId
            client_secret = $this.clientSecret
            scope         = $this.scopeUrl
            redirect_uri  = $this.callbackUrl
            resource      = $this.resourceUrl
        }
        $authResult = Invoke-RestMethod -Method Post -Uri $this.accessTokenUrl -Body $body
        $this.token =$authResult.access_token
    }
}

#
# Test the Class Here
#
$tenantId     = '1447d526-aee2-47e9-8418-27732e6848546'      #Your BC tennantID
$clientId     = 'af912c1d-d67c-407a-8f5f-2bd8dbb1cda9'      #The id that BC is registered with in AAD  
$clientSecret = 'ETZ9Nqy--Wi9-zjF9B97-.oeCLyA0.AEfd' #The secret key that was created when registering BC for AAD Auth
$username     = 'xx@xxx.com'            #Username for the password grant -- this is usually your Office365 credentials
$password     = 'your office365 password'                       #Password in plain text note.. this is not advised and there are ways of producing a key in PowerShell so your password is not stored in plain text

[AADPasswordGrant]$aadPasswordGrant = [AADPasswordGrant]::new($tenantId, $clientId, $clientSecret, $username, $password)
$aadPasswordGrant.TryGetAuthorisationToken()

$companiesUrl = "https://api.businesscentral.dynamics.com/v2.0/Production/api/v2.0"
$requestHeaders = @{ 'Authorization' = 'Bearer ' + $aadPasswordGrant.token }
$result = Invoke-RestMethod -Uri $companiesUrl -Headers $requestHeaders -Method Get
$result.value | Export-Csv -Path c:\output\bc-environment.csv

A few things to note about our environment:

  1. We are running Business Central (SAAS/Online – it’s in Office365 not our own servers
  2. We use AAD (Azure Active Directory) to authenticate – that is we use our Office365 credentials to login to Business Central (I am using the OAuth2.0 token to gain access to Business Central
  3. The code above is accessing the PRODUCTION platform.

Most of the code above is just to get the OAuth 2.0 token to get access to Business Central. Why is it not simply a username and password? I’m not sure, and this is what Microsoft requires to access Business Central. Like I said, I’m not a coder, just an accountant.

There are 5 pieces of information that you need to change in the above code (and you might not be able to get all of them yourself):

  1. Tenant ID – You can get this by going to Business Central with your browser and clicking on Help and Support. You will find your Tenant ID. If you don’t see it, you can always get it from your URL.
The red underline is the Tenant ID

2.