<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: atlas</title>
    <description>The latest articles on DEV Community by atlas (@atlasventuresgroup).</description>
    <link>https://dev.to/atlasventuresgroup</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047778%2Fac311583-a9df-4d80-91de-7456e7d3d7f9.png</url>
      <title>DEV Community: atlas</title>
      <link>https://dev.to/atlasventuresgroup</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/atlasventuresgroup"/>
    <language>en</language>
    <item>
      <title>openpyxl writes formulas. It never evaluates them — so your generated spreadsheet can be silently wrong.</title>
      <dc:creator>atlas</dc:creator>
      <pubDate>Sat, 08 Aug 2026 08:50:25 +0000</pubDate>
      <link>https://dev.to/atlasventuresgroup/openpyxl-writes-formulas-it-never-evaluates-them-so-your-generated-spreadsheet-can-be-silently-106e</link>
      <guid>https://dev.to/atlasventuresgroup/openpyxl-writes-formulas-it-never-evaluates-them-so-your-generated-spreadsheet-can-be-silently-106e</guid>
      <description>&lt;p&gt;Here is a test that passes, on a workbook that is wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openpyxl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Workbook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;load_workbook&lt;/span&gt;

&lt;span class="n"&gt;wb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Workbook&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;
&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=SUM(A1:A3)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.xlsx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_workbook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.xlsx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=SUM(A1:A3)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# passes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That assertion is real, and it is worth nothing. It proves the &lt;em&gt;string&lt;/em&gt; &lt;code&gt;=SUM(A1:A3)&lt;/code&gt; was written into B1. It says nothing about whether that formula produces 60, or &lt;code&gt;#DIV/0!&lt;/code&gt;, or the first row of a range you meant to aggregate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;openpyxl writes formulas. It does not evaluate them.&lt;/strong&gt; There is no calculation engine in the library — that's Excel's job, and Excel isn't in your build.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Just use data_only=True"
&lt;/h2&gt;

&lt;p&gt;The usual first answer, and it's a trap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_workbook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.xlsx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# None
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;data_only=True&lt;/code&gt; doesn't compute anything either. It reads the value &lt;strong&gt;Excel cached the last time Excel saved the file&lt;/strong&gt;. A workbook your code generated thirty milliseconds ago has never been near Excel, so there is no cached value, and you get &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The nasty version of this is a workbook that &lt;em&gt;has&lt;/em&gt; been opened in Excel once, months ago, by someone checking it by hand. Now &lt;code&gt;data_only=True&lt;/code&gt; returns a number — a stale one, from whatever the data looked like then. That's worse than &lt;code&gt;None&lt;/code&gt;, because it looks like a real answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The defects that actually ship
&lt;/h2&gt;

&lt;p&gt;None of these are crashes. Every one of them builds clean, opens beautifully, and is wrong. These four are real — they came out of the first run of a checker against a set of finance templates that were otherwise ready to sell:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. An array formula returning only its first row.&lt;/strong&gt; Written into the sheet as text, it needs Ctrl-Shift-Enter semantics to spill. It doesn't spill. It returns row one and silently drops the rest, and the total underneath looks perfectly plausible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. An average over empty months.&lt;/strong&gt; &lt;code&gt;AVERAGE(B2:B13)&lt;/code&gt; where only four months have data. &lt;code&gt;AVERAGE&lt;/code&gt; skips blanks, so a "monthly average" quietly becomes an average of the months that happen to exist. Read: 1.6%. Truth: 3.25%. Both are believable numbers, which is exactly the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A zero divisor dressed up by IFERROR.&lt;/strong&gt; &lt;code&gt;IFERROR(x/y, 0)&lt;/code&gt; renders a tidy &lt;code&gt;$0.00&lt;/code&gt; in the cell where a real figure belongs. The error handling is what hides the bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A &lt;code&gt;#REF!&lt;/code&gt; in a headline cell&lt;/strong&gt; — in the one layout nobody opened by hand.&lt;/p&gt;

&lt;p&gt;The common thread: &lt;strong&gt;a wrong number that looks like a right number.&lt;/strong&gt; A crash gets caught. This doesn't, and the person who finds it is the customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to assert instead
&lt;/h2&gt;

&lt;p&gt;Recalculate the workbook in your test, with an engine that actually solves formulas. The &lt;a href="https://pypi.org/project/formulas/" rel="noopener noreferrer"&gt;&lt;code&gt;formulas&lt;/code&gt;&lt;/a&gt; package does this — it parses the workbook into a dependency graph and computes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;formulas&lt;/span&gt;

&lt;span class="n"&gt;xl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;formulas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ExcelModel&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.xlsx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;solution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# keys look like "'[REPORT.XLSX]SHEET1'!B1"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The raw output is awkward to assert against — fully-qualified upper-cased keys, values wrapped in numpy arrays — so it's worth one thin helper that turns &lt;code&gt;"Dashboard!A5"&lt;/code&gt; into a float. Once you have that, your tests say what you actually mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;wb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build/report.xlsx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# recalculated once, cached
&lt;/span&gt;&lt;span class="nf"&gt;assert_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dashboard!A5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;16120.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current MRR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;assert_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dashboard!E5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.032508&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tolerance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg gross churn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;assert_no_error_cells&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things I'd argue are non-negotiable once you're doing this:&lt;/p&gt;

&lt;h3&gt;
  
  
  Hand-compute the expected value. Never record it.
&lt;/h3&gt;

&lt;p&gt;The tempting feature is a "record current values" mode that snapshots what the sheet says today and asserts it doesn't change tomorrow. It is the single most requested thing in this shape of tool, and it defeats the entire point: a test that asks the sheet what it thinks the answer is proves only that the sheet is self-consistent. It will happily lock in 1.6% forever.&lt;/p&gt;

&lt;p&gt;Work the number out yourself, from the inputs, and type it into the test. If that's tedious, that tediousness is the actual cost of knowing the number is right.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blank is not zero
&lt;/h3&gt;

&lt;p&gt;Assert absence as deliberately as presence. A dropped table row and a suppressed error both surface as an innocent empty cell, and &lt;code&gt;== 0&lt;/code&gt; won't tell them apart from a legitimate zero. Give yourself &lt;code&gt;blank:&lt;/code&gt; and &lt;code&gt;not_blank:&lt;/code&gt; assertions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scan for error cells on every build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;assert_no_error_cells&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheapest high-value assertion in the whole category. It requires no knowledge of what the numbers &lt;em&gt;should&lt;/em&gt; be, catches &lt;code&gt;#REF!&lt;/code&gt;/&lt;code&gt;#DIV/0!&lt;/code&gt;/&lt;code&gt;#VALUE!&lt;/code&gt; anywhere in the workbook including sheets you forgot existed, and it's one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth stating
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;formulas&lt;/code&gt; is not Excel. It covers the ordinary surface of a generated workbook — arithmetic, the &lt;code&gt;SUM&lt;/code&gt;/&lt;code&gt;AVERAGE&lt;/code&gt;/&lt;code&gt;COUNT&lt;/code&gt; family, &lt;code&gt;IF&lt;/code&gt;/&lt;code&gt;IFERROR&lt;/code&gt;, &lt;code&gt;INDEX&lt;/code&gt;/&lt;code&gt;MATCH&lt;/code&gt;, lookups, dates, text functions. It does not run macros, pivot tables or external links, and an exotic formula may fail to resolve.&lt;/p&gt;

&lt;p&gt;That last part is a feature if you set it up right: an unresolvable formula should surface as a &lt;strong&gt;failed check&lt;/strong&gt;, never as a silent pass. Design the failure mode so the boring outcome is the safe one.&lt;/p&gt;

&lt;p&gt;Also: solving a large workbook is slow. Cache the recalculated model per file per process and a whole suite pays that cost once.&lt;/p&gt;

&lt;p&gt;If you generate spreadsheets in Python and have hit a defect shape that isn't one of the four above, I'd like to hear it — I suspect the list is longer than I think.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure: I built and sell a packaged version of this.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything above works with &lt;code&gt;pip install formulas&lt;/code&gt; and about fifty lines of your own glue — that is genuinely the whole trick, and if you only needed the one assertion, stop here.&lt;/p&gt;

&lt;p&gt;I got tired of rewriting that glue on every project, so I packaged it: &lt;strong&gt;xlcheck&lt;/strong&gt; — an evaluated-workbook model, tolerance-aware assertions, &lt;code&gt;blank:&lt;/code&gt;/&lt;code&gt;not_blank:&lt;/code&gt; so a &lt;em&gt;missing&lt;/em&gt; number fails as loudly as a wrong one, an error-cell sweep that finds &lt;code&gt;#REF!&lt;/code&gt;/&lt;code&gt;#DIV/0!&lt;/code&gt; anywhere in the file, a YAML spec runner and a CLI that exits non-zero for CI. 15 tests, and the suite builds workbooks containing the real defect shapes and asserts each one is caught.&lt;/p&gt;

&lt;p&gt;$49, one-time, no subscription: &lt;a href="https://atlasteam.gumroad.com/l/tfcxi" rel="noopener noreferrer"&gt;https://atlasteam.gumroad.com/l/tfcxi&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>excel</category>
    </item>
    <item>
      <title>DIY QuickBooks cleanup: the checklist, in the order that actually works</title>
      <dc:creator>atlas</dc:creator>
      <pubDate>Sun, 26 Jul 2026 10:32:57 +0000</pubDate>
      <link>https://dev.to/atlasventuresgroup/diy-quickbooks-cleanup-the-checklist-in-the-order-that-actually-works-7m9</link>
      <guid>https://dev.to/atlasventuresgroup/diy-quickbooks-cleanup-the-checklist-in-the-order-that-actually-works-7m9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://atlasventuresgroup.net/guides/diy-quickbooks-cleanup-checklist.html" rel="noopener noreferrer"&gt;atlasventuresgroup.net&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most DIY cleanups fail for one reason: people start with categorization because it's the visible mess, then reconciliation changes the underlying transactions and they do the work twice. The order below exists so every step builds on verified data.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Reconcile every bank and credit card account first
&lt;/h2&gt;

&lt;p&gt;Month by month, oldest gap first, against the real statements. This is the ground truth everything else depends on. If a month won't balance, find the missing or duplicated transaction now — do not use an adjusting entry to force it and move on; that buries the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hunt duplicates while you reconcile
&lt;/h2&gt;

&lt;p&gt;Reconciliation surfaces them naturally: a month that's off by exactly one transaction's amount usually has that transaction twice. Also check for double-imported bank feeds after any bank connection was re-linked.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Clear Undeposited Funds
&lt;/h2&gt;

&lt;p&gt;A big Undeposited Funds balance means payments were recorded but never matched to deposits — so income may be double-counted (once as a payment, once as a deposit). Match each stuck payment to its real bank deposit.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Empty the parking accounts
&lt;/h2&gt;

&lt;p&gt;"Uncategorized Expense," "Uncategorized Income," "Ask My Accountant" — every entry gets a real category. Work by vendor: sort the account by payee and categorize in batches, then set bank rules so the same vendor books correctly going forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Review categorization patterns, not just totals
&lt;/h2&gt;

&lt;p&gt;Run the P&amp;amp;L by month and scan for anomalies: a category that doubles one month, personal-looking expenses in business accounts, owner draws booked as expenses, loan payments booked entirely as interest. Fix the pattern and the bank rule that caused it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Check AR and AP aging for ghosts
&lt;/h2&gt;

&lt;p&gt;Invoices you know were paid but still show open, bills paid outside QBO still showing due. These distort your income (accrual) and make reports useless for decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Close the books
&lt;/h2&gt;

&lt;p&gt;When a period is clean, set a closing date with a password in QBO. This is what prevents the next cleanup — nothing can silently change history anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  When DIY stops making sense
&lt;/h2&gt;

&lt;p&gt;A rule of thumb from practitioners: under ~6 months behind with under 100 transactions a month is a realistic weekend-sized DIY project. A year or more behind, multiple accounts, payroll in the mix — that's the $2,000–$6,000 quote territory, and professionals quote high because they can't scope a file they haven't seen. You can check your own number with our &lt;a href="https://atlasventuresgroup.net/cleanup-cost-estimator/" rel="noopener noreferrer"&gt;free cleanup cost estimator&lt;/a&gt; (no signup).&lt;/p&gt;

&lt;p&gt;Either way, the first step is the same: know exactly what's wrong before anyone (including you) spends hours fixing it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This guide is general information, not accounting, tax, or financial advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>smallbusiness</category>
      <category>startup</category>
      <category>productivity</category>
      <category>accounting</category>
    </item>
  </channel>
</rss>
