Photos go in a folder. Product pages come out.

A wholesale distributor with a catalog that changes every week. Someone drops a product photo and its lab report into Google Drive. Fifteen minutes later the ERP has the report attached, the team's spreadsheet has the new row, the customer photo folder is updated, and the website has a product page with photo, lab results, a written description, and related products. Nobody touched anything.

Elevated Trading · 2025–2026 · Python · runs every 15 minutes on a cloud VM · ← all work

The problem

A new product needed a photo edited, a lab report pulled, inventory updated, then a page built by hand on the website, in the spreadsheet, and in the ERP. Three places, three chances to drift, one person's afternoon. And nobody noticed when a lab report expired.

What I built

One scheduled job that watches Drive for the photo, the report, and a stock quantity. When all three are there it builds the page, updates the ERP, mirrors the photo for customers, and emails whoever added the product what's still missing and which reports are about to expire.

What changed

The manual three-location update per product is gone. The expiring-report problem is gone. The person who used to spend the afternoon now spends it on customers, and the company kept the system after I moved on.

The mechanism

Every fifteen minutes one script walks its steps in order. On the left is what it reads. In the middle are the steps. On the right is what the company sees. A small local store in the middle is written once per tick and read by every later step, so the site, the sheet, and the customer folder never disagree with each other.

INPUTS GitHub · master the code, read-only deploy key Shared Drive pending photos · the COA folders Odoo stock · variants · orders the ERP, cloud-hosted Claude API Haiku, one blurb per new strain Upsell sheet the maintainer's related-product picks THE TICK · every 15 minutes, prod host only 0pull the latest code 1mirror the Drive folders 2extract COA data → the store 3sync inventory → the store 3.5attach COAs in Odoo 4mirror media to InStock 4.5write blurbs → _Descriptions 5reconcile WooCommerce 5.5push upsells endnotify on a change each step non-fatal; failures named in sync.log git pull API mirror stock newest COA blurbs picks OUTPUTS Inventory store inventory.db + inventory.json products · label ready · coa meta: tick time, tier order written at 2 and 3, inside the lock read by 4 · 5 · 5.5 and This Just-in COA rows stock rows Live Ref sheet This Just-in · Notes Categories · _Descriptions the sheet the team reads Just-in new blurbs → the _Descriptions tab InStock folder customer-facing media photos elevatedtrading.com WooCommerce pages create · show · hide photo · COA · upsells related products page per in-stock strain, from the store Email 8am digest · failure notices only when the set of failing steps changes

Each step is non-fatal. A failure is named in the log, and an email goes out only when the set of failing steps changes, so nobody gets paged for a known blip.

Step by step, in plain words

  1. Pull the latest code. The host updates itself from the repository before every run. Shipping a fix is a push; it's live within fifteen minutes.
  2. Mirror the Drive folders. Pending product photos and the lab-report folders are copied down so the rest of the run works on local files.
  3. Read the lab reports. Cannabinoid percentages and dates are extracted from each PDF into the store. Reports older than a year are flagged.
  4. Sync inventory. Stock, variants, and orders come from the ERP into the same store.
  5. Attach reports in the ERP. The newest report for each product is attached to its record, so the ERP is the source of truth for compliance too.
  6. Mirror media for customers. In-stock photos land in a shared folder the sales team sends to customers.
  7. Write descriptions. A new strain gets a short blurb from Claude, once. Existing blurbs are reused, so the API bill stays flat.
  8. Reconcile the website. Every in-stock product gets a page with photo, lab results, and description. Out-of-stock pages are hidden, not deleted.
  9. Push related products. The maintainer's picks from a spreadsheet become the "you might also like" strip.
  10. Notify. A morning digest and, only on change, a failure notice.

How it stays alive after I leave

The whole system is one repository with one Python package underneath it. Connecting to the ERP, authenticating to Google, knowing which sheet is which: each of those lives in exactly one place instead of being copied into every job. The production host pulls the repository at the top of every run, so a change moves from a developer's laptop to production without anyone logging into the server.

A developer's fork branch · edit · pytest GitHub · master a maintainer merges Production host step 0: git pull, every tick pull request live ≤ 15 min a hand edit here = dirty tree= the pull stops, silently

The one rule: never edit on the server. A hand edit makes the tree dirty and the self-update silently stops. The docs say so on page one.

The documentation is written for a reader who has never seen the system: what it is, how to install it, how to authenticate, what each job does, how to add a report, how to test end to end, what to do when something breaks. That's the part that makes it a handoff instead of a hostage situation.

Stack

Python 3 SQLite Odoo XML-RPC Google Drive + Sheets API WooCommerce REST Claude API systemd timers Ubuntu VM GitHub

The front of the pipeline is a small desktop app that cleans up the raw photos before they go in the folder. That one is open source: PhotoEditor. The back of it, the WooCommerce half, was worked out first on a spin-off's order portal, where a new product got its own product page and its photo from that same editor. Bringing that back to Elevated is what turned a photo tool into this pipeline.

Built in conversation

I'm not a career software engineer. This was designed, written, debugged, and documented through dialogue with Claude, one decision at a time, over about a year of evenings alongside a day job. The hard parts weren't the code. They were deciding what the source of truth is, what happens when a step fails at 2am, and how to hand the whole thing to someone else. Those are conversations, and in 2026 you can have them with a machine that never gets tired of the follow-up question.

Same approach behind everything on this site. The philosophy, or what I'm building right now.