Devlly

get in touch

Devlly - a software studio. We automate business: from a Telegram bot to a full CRM/ERP system.

Get updates

  • Case
  • Finance and analytics

MarginTracker - a margin report from three CRMs and Nova Poshta

MarginTracker is a Windows app that does in one pass what used to take working days. It collects orders from the two CRM systems the online stores feed into, fills in product cost, pulls the shipping cost of every return out of Nova Poshta, and writes the finished report into a Google Sheet - one row per store plus a grand total.

Who it fits: product businesses running several stores whose orders live in different CRMs, where some parcels come back and the margin is still pieced together in Excel by hand. The more orders a month, the more the manual work costs - and the more an avoided mistake is worth.

Stack: Python 3, CustomTkinter for the interface, requests for the APIs, openpyxl for the local cost storage, gspread and google-auth for writing to the sheet. Packaged into a single exe with PyInstaller - nothing to install on the client’s machine.

  • Python 3
  • CustomTkinter
  • requests
  • openpyxl
  • gspread
  • PyInstaller

The problem

The client is a product business running several online stores. Leads from landing pages fall into two different CRM systems, the goods travel with Nova Poshta and some parcels come back. To find out what the month actually earned, the report was assembled by hand: export the orders from each CRM, glue them together in Excel, fill in the cost and work out separately how much the returns ate. For a few thousand orders that took working days, and every number carried over by hand was a chance to slip.

The hard part is not the arithmetic. It is that the data has to be collected from three different APIs with different limits, that the same product and the same store are spelled differently in each of them, and that the cost of return shipping exists neither in the CRM nor in any report the carrier provides.

How it works

The app walks the user through three steps, and you cannot move on before the current one is done. Collect - cost - report. Everything runs locally in a desktop window: a single exe built with PyInstaller, nothing to install.

Every figure, store, product and number in the screenshots below is invented. This is the app’s demo mode, where all three steps can be walked through without a single API key: the interface and the calculations are real, the data was generated for this portfolio and has nothing to do with the client’s own.

1. Collecting the orders

The user picks a period and presses

Margin reporting app - the order collection step with a progress bar and an execution log
Step 1 while collecting: the chosen period, the progress bar and a log per source. While the collection runs the navigation is locked - you cannot move on with half the data.
Collection summary: the order count for each CRM account, the number of stores and unique products
The same screen once collection is done: how many orders each source returned on its own - two LP-CRM accounts and SalesDrive - plus the number of stores and unique products for the period.

2. Product cost

The second step is an editable table of every product in the period. Whatever was filled in last month is pre-filled; new items are highlighted yellow so they cannot be missed. The entered prices go into a local xlsx and survive between runs, and writes to the local files are atomic: an interruption halfway through does not corrupt the storage.

The product cost table: items carried over from previous months and new ones highlighted in yellow
Step 2: the editable cost table. Items filled in before are pulled in automatically, new ones are highlighted yellow. There is a search box, a
The
The same screen with a

3. The report

On the third step the app calculates the metrics, pulls in the delivery cost of the returns and shows the exact content that will go into the sheet. One row per store plus a grand total: the number of leads and of collected orders, revenue, cost, upsell margin, main margin, the number of returns and their delivery cost, plus how many waybills went uncounted and how many products are still missing a cost.

The margin is split in two on purpose. Advertising sells the main product, the operator sells the upsell - for the business these are different money and they are looked at separately. Order statuses are folded into three groups through a config: successful ones go into revenue, returns are counted separately, the rest is deliberately ignored. A status that belongs to no group breaks nothing: the app finishes the report and lists the new statuses as a separate remark.

Writing into the Google Sheet goes through a service account and in batches - a few requests for the whole report rather than row by row, otherwise you hit the API quota. The sheet can be created fresh every month or appended to.

The calculated financial report: revenue, cost, split margin and delivery costs on returns
Step 3: the report is calculated. Revenue, cost, margin split between the main product and upsells, the number of returns and how much their delivery ate. Below it - the remarks the program found by itself.
A preview of the Google Sheet contents: one row per store plus a grand total row
The same screen scrolled down: a warning about double-counted waybills and the exact content that will go into the Google Sheet - one row per store and a grand total with every column of the report.

Two problems the API documentation does not mention

The most valuable part of this project is not the interface. It is two things with no API method and no line of documentation behind them, which had to be worked out from scratch.

Disappearing orders: order_id collisions

The symptom: the client kept saying a few orders were missing from the report every month. Going through the logs showed the CRM API really did return fewer records than it was asked for.

The cause turned out to be the number generator on the landing page: the number is built from the timestamp down to a tenth of a second. Two orders placed within the same tenth of a second get the same number, and the API returns only one of them. Worse - inside a batch of a hundred numbers such an order deterministically eats one more record, the largest number in the batch, because of how the CRM limits the selection after joining its tables.

What was done. The eaten record is caught by repeating the request in smaller batches. For genuine duplicates there is a manual stage: the app works out how many orders live under one number and opens a window where the manager types in what the API withheld. What is typed is kept forever and reused in later runs - but only while the collision is still there, the missing order’s status still matches and the API still returns the same order as it did when the form was filled. If any of that changed, the record is set aside and shown to the manager again instead of quietly reusing a stale figure. The cause was passed back to the client: on some of the landing pages the number generator already adds a random suffix, and there the collision count is exactly zero.

The manual stage for order_id collisions: the versions living under one number and the input form
The manual stage for collisions. On the left, for every number, what the API actually returned and what is missing. On the right, the order already received and a form whose fields change with the chosen status: for a sale that is the store, the product, the quantity and the amount.

The cost of return shipping

The CRM does not hold this figure at all, and the carrier exposes it through no dedicated method. Three places were checked: the return requests, the list of own waybills and the price calculator. In the first two the cost field is empty, the third does not accept a waybill number.

The scheme that works is this: the cost sits in the waybill card, and the full price of a return is the outbound leg plus the return leg plus the paid storage at the branch, which starts after seven free days. The carrier’s dashboard shows that storage on both waybills, so it has to be counted once per parcel or the sum doubles. The scheme was reconciled against a control sample of waybills down to the kopeck.

The app does not pretend the data is clean

Before writing the report the app shows whatever could make the figures inaccurate: products without a cost, orders without products, returns with no waybill or with one the carrier does not know, the same waybill on two orders, new statuses, unfilled collisions. The calculation does not stop - you can look at the examples, accept them as they are, or go back and fix them.

The reasoning is simple: a report over a few thousand orders is either right or worthless. So the user should see exactly where a number may be off, rather than get a silent

The report remarks dialog: products without a cost and unprocessed collisions with examples
The problem review before writing. The calculation does not stop - the program shows exactly what could make the figures inaccurate, with concrete examples, and offers a choice: accept as is, or go back and fix.

The logic that matters

  • → Manual collision entries are re-validated on every run: if the status or the API response changed, the entry is set aside and shown to a human instead of being reused silently
  • → Paid storage at the branch is counted once per parcel even though the carrier shows it on both waybills - otherwise the cost of a return doubles
  • → The margin is split into main and upsell: different people generate them, so they are looked at separately
  • → An unknown order status does not break the calculation - the report is finished and the new statuses go into the remarks
  • → Writing to the Google Sheet is batched: a few requests for the whole report instead of row by row, otherwise the API quota bites
  • → Atomic writes to the local files: an interruption mid-save does not corrupt the cost storage or the manual entries
  • → API errors reach the user in plain language rather than as a traceback, and the request is retried after a pause

What the app can do

  • → Collecting a period’s orders from two CRM systems - two LP-CRM accounts and SalesDrive - in one pass
  • → Respecting request limits with pauses and a check on the remaining quota
  • → Mapping custom order fields through a config, to the names of the specific account
  • → An editable cost table with search, a missing-only filter and new products highlighted
  • → Carrying cost between months: whatever was filled in before is applied automatically
  • → A manual stage for order_id collisions, with the entries re-validated on every later run
  • → Calculating the delivery cost of every return from the Nova Poshta waybills
  • → A financial report per store and in total, with the margin split and the full return statistics
  • → Data remarks before writing - with examples and a choice between accepting them and fixing them
  • → Writing into a formatted Google Sheet: a fresh sheet every month or an append to an existing one
  • → A demo mode: all three steps can be walked through on generated data without a single API key
  • → An in-house self-test suite of 354 checks, run with a single command, that catches regressions in the calculations, the status mapping and the manual-entry logic

Still building the report by hand? Tell us about your data and we will see what can be automated